Python Debugger (pdb)

デバッグPythonコマンドラインステップ実行ブレークポイントインタラクティブ

デバッグツール

Python Debugger (pdb)

概要

Python Debugger(pdb)は、Pythonの標準デバッガーです。コマンドライン操作でPythonプログラムの実行を制御し、ブレークポイント設定、変数の確認、ステップ実行が可能です。

詳細

Python Debugger(pdb)は、Pythonの標準ライブラリに含まれる対話型デバッガーで、1990年代からPython言語と共に発展してきました。Pythonインタープリターに組み込まれているため、追加のインストールは不要で、シンプルなコマンドライン インターフェースを通じて強力なデバッグ機能を提供します。pdbは、GDBのPythonポートとして設計され、類似のコマンド体系を持ちながらPythonの動的な性質に最適化されています。

pdbの特徴は、実行中のPythonプログラムに対する完全なインタラクティブ制御です。任意のポイントでプログラムの実行を一時停止し、変数の値確認、関数呼び出し、式の評価、コールスタックの検査が可能です。post-mortem デバッグ機能により、例外発生後にプログラムの状態を詳細に分析できます。また、リモートデバッグ機能を使用して、別のプロセスやネットワーク越しのPythonプログラムをデバッグすることも可能です。

IDEの高機能デバッガーが普及する中でも、pdbは軽量性とシンプルさで根強い支持を得ています。特に、本番環境やリモートサーバーでの問題調査、CIパイプラインでのデバッグ、Dockerコンテナ内でのトラブルシューティングなど、GUI環境が利用できない状況で威力を発揮します。Python 3.7以降ではbreakpoint()ビルトイン関数が追加され、pdbの利用がさらに簡素化されました。

pdb++(pdbpp)やIPDBなど、pdbを拡張したサードパーティツールも存在し、構文ハイライト、オートコンプリート、改善されたユーザーインターフェースを提供しています。現在のPython開発において、基本ツールとしての地位を保ち続けています。

メリット・デメリット

メリット

  • 標準搭載: Pythonインストール時から利用可能、追加設定不要
  • 軽量性: 最小限のリソース消費でデバッグ実行
  • シンプルな操作: 直感的なコマンドライン インターフェース
  • 完全な制御: プログラム実行の詳細な制御と検査
  • post-mortem分析: 例外発生後の詳細な状態分析
  • リモートデバッグ: ネットワーク経由でのデバッグ可能
  • IDEに依存しない: 任意の環境で一貫したデバッグ体験
  • 拡張性: カスタムコマンドとスクリプト機能

デメリット

  • 学習コスト: コマンドライン操作の習得が必要
  • GUI不足: 視覚的なインターフェースの欠如
  • 機能制限: IDEデバッガーと比較して機能が限定的
  • 効率性: 大規模プログラムでの操作効率の低下
  • 表示制限: 複雑なデータ構造の可視化が困難
  • 統合不足: 現代的な開発ツールとの統合が弱い
  • 初心者の敷居: Python初心者には操作が困難

主要リンク

書き方の例

pdbの基本的な起動方法

# 方法1: プログラム開始時にpdbで起動
# python -m pdb script.py

# 方法2: Python 3.7以降のbreakpoint()関数
def problematic_function(data):
    result = []
    for item in data:
        breakpoint()  # ここでデバッガー起動
        processed = item * 2
        result.append(processed)
    return result

# 方法3: 従来のpdb.set_trace()
import pdb

def another_function():
    x = 10
    y = 20
    pdb.set_trace()  # ここでデバッガー起動
    z = x + y
    return z

# 方法4: post-mortem デバッグ
try:
    # エラーが発生する処理
    result = 1 / 0
except:
    import pdb
    pdb.post_mortem()  # 例外発生時点の状態でデバッグ開始

基本的なpdbコマンド

# デバッグ対象のサンプルコード
def calculator(operation, a, b):
    if operation == 'add':
        return a + b
    elif operation == 'subtract':
        return a - b
    elif operation == 'multiply':
        return a * b
    elif operation == 'divide':
        if b == 0:
            raise ValueError("Division by zero")
        return a / b
    else:
        raise ValueError("Unknown operation")

def main():
    operations = [
        ('add', 10, 5),
        ('subtract', 10, 3),
        ('multiply', 4, 7),
        ('divide', 10, 0)  # エラーが発生する
    ]
    
    for op, x, y in operations:
        breakpoint()  # 各反復でデバッグ
        try:
            result = calculator(op, x, y)
            print(f"{op}({x}, {y}) = {result}")
        except ValueError as e:
            print(f"Error: {e}")

# pdbコマンド例:
# (Pdb) l          # 現在の行周辺のコード表示
# (Pdb) n          # 次の行へ(ステップオーバー)
# (Pdb) s          # ステップイン(関数内に入る)
# (Pdb) c          # 実行継続
# (Pdb) p op       # 変数opの値表示
# (Pdb) pp operations  # 変数operationsの整形表示
# (Pdb) w          # 現在のスタックフレーム表示
# (Pdb) u          # 上位スタックフレームに移動
# (Pdb) d          # 下位スタックフレームに移動

高度なpdbコマンドの使用

class DataProcessor:
    def __init__(self, data):
        self.data = data
        self.processed_count = 0
    
    def process_items(self):
        results = []
        for i, item in enumerate(self.data):
            # 条件付きブレークポイント設定例
            if i == 2:
                breakpoint()
            
            processed = self._process_single_item(item)
            results.append(processed)
            self.processed_count += 1
        
        return results
    
    def _process_single_item(self, item):
        if isinstance(item, str):
            return item.upper()
        elif isinstance(item, (int, float)):
            return item ** 2
        else:
            return str(item)

# 高度なpdbコマンド例:
# (Pdb) break DataProcessor.process_items  # メソッドにブレークポイント
# (Pdb) break 15, i > 1  # 条件付きブレークポイント(15行目、i > 1の時のみ)
# (Pdb) tbreak 20       # 一時的ブレークポイント(一度だけ停止)
# (Pdb) enable/disable 1  # ブレークポイント1の有効/無効切り替え
# (Pdb) clear           # 全ブレークポイント削除
# (Pdb) ignore 1 2      # ブレークポイント1を2回無視

processor = DataProcessor(['hello', 42, 'world', 3.14, None])

変数検査とコード実行

def complex_calculation(data_list):
    import math
    
    results = {
        'sum': 0,
        'mean': 0,
        'std_dev': 0,
        'processed_items': []
    }
    
    # 数値のみフィルタリング
    numeric_data = [x for x in data_list if isinstance(x, (int, float))]
    breakpoint()  # 検査ポイント
    
    if not numeric_data:
        return results
    
    # 統計計算
    total = sum(numeric_data)
    mean = total / len(numeric_data)
    
    variance = sum((x - mean) ** 2 for x in numeric_data) / len(numeric_data)
    std_dev = math.sqrt(variance)
    
    results.update({
        'sum': total,
        'mean': mean,
        'std_dev': std_dev,
        'processed_items': numeric_data
    })
    
    return results

# pdbでの変数検査コマンド:
# (Pdb) p data_list              # リスト内容表示
# (Pdb) pp results               # 辞書の整形表示
# (Pdb) len(numeric_data)        # 式の評価
# (Pdb) [x for x in data_list if x > 10]  # リスト内包表記の実行
# (Pdb) type(data_list[0])       # 型確認
# (Pdb) dir(math)                # モジュール属性一覧
# (Pdb) help math.sqrt           # 関数ヘルプ

# 実行中に変数値変更:
# (Pdb) numeric_data.append(100)  # リストに要素追加
# (Pdb) results['debug'] = True   # 辞書に要素追加

test_data = [1, 2, 'three', 4.5, None, 6, 'seven', 8.9]

スタックフレーム操作とデバッグ

def outer_function(x):
    print(f"Outer function called with {x}")
    result = middle_function(x * 2)
    return result

def middle_function(y):
    print(f"Middle function called with {y}")
    if y > 10:
        return inner_function(y - 5)
    else:
        return y

def inner_function(z):
    print(f"Inner function called with {z}")
    breakpoint()  # 最も深い関数でデバッグ開始
    return z * z

# スタックフレーム操作コマンド:
# (Pdb) where / w    # 完全なスタックトレース表示
# (Pdb) up / u       # 上位フレーム(呼び出し元)に移動
# (Pdb) down / d     # 下位フレーム(呼び出し先)に移動
# (Pdb) frame 0      # 特定フレーム番号に移動
# (Pdb) args         # 現在のフレームの引数表示
# (Pdb) locals()     # ローカル変数一覧
# (Pdb) globals()    # グローバル変数一覧

result = outer_function(3)

エラーハンドリングとpost-mortem デバッグ

def risky_operation(data):
    """エラーが発生する可能性のある処理"""
    results = []
    
    for i, item in enumerate(data):
        try:
            if isinstance(item, str):
                # 文字列を数値に変換
                number = float(item)
            elif isinstance(item, (int, float)):
                number = item
            else:
                raise TypeError(f"Unsupported type: {type(item)}")
            
            # 0による除算のリスク
            result = 100 / number
            results.append(result)
            
        except (ValueError, TypeError) as e:
            print(f"Error processing item {i}: {e}")
            # エラー時にデバッガー起動
            breakpoint()
            continue
        except ZeroDivisionError:
            print(f"Division by zero for item {i}")
            breakpoint()
            continue
    
    return results

# post-mortem デバッグの使用例
def main_with_error_handling():
    test_data = ['10.5', 20, '0', 'invalid', 5.5, None]
    
    try:
        results = risky_operation(test_data)
        print(f"Results: {results}")
    except Exception:
        # 未処理例外発生時にpost-mortem デバッグ
        import pdb
        pdb.post_mortem()

# post-mortem デバッグで可能な操作:
# (Pdb) l            # エラー発生箇所のコード確認
# (Pdb) p item       # エラー発生時の変数値
# (Pdb) p i          # ループカウンター
# (Pdb) pp data      # 全データの確認
# (Pdb) type(item)   # エラーを起こした要素の型

リモートデバッグ設定

# リモートデバッグ用のサーバープログラム
import pdb
import socket
import sys

def remote_debugger_setup(host='localhost', port=4444):
    """リモートデバッグ用のpdb設定"""
    class RemotePdb(pdb.Pdb):
        def __init__(self, host, port):
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.bind((host, port))
            self.socket.listen(1)
            print(f"Waiting for debugger connection on {host}:{port}")
            
            client_socket, address = self.socket.accept()
            print(f"Debugger connected from {address}")
            
            # ソケットをファイルオブジェクトとして使用
            self.handle = client_socket.makefile('rw')
            pdb.Pdb.__init__(self, stdin=self.handle, stdout=self.handle)
    
    return RemotePdb(host, port)

def server_function_with_debug():
    data = [1, 2, 3, 4, 5]
    
    # リモートデバッガー起動
    debugger = remote_debugger_setup()
    debugger.set_trace()
    
    # 実際の処理
    processed = [x * 2 for x in data]
    return processed

# クライアント側からの接続:
# telnet localhost 4444

pdb++とIPDBの使用例

# pdb++のインストールと使用
pip install pdbpp

# IPDBのインストールと使用
pip install ipdb
# pdb++の場合(自動的にpdbを置き換え)
def enhanced_debug_example():
    data = {'a': 1, 'b': [2, 3, 4], 'c': {'nested': True}}
    breakpoint()  # pdb++が起動(構文ハイライト、補完機能)
    result = process_data(data)
    return result

# IPDBの場合
def ipdb_example():
    import ipdb
    
    numbers = list(range(10))
    ipdb.set_trace()  # IPython統合デバッガー起動
    
    # IPythonの豊富な機能が利用可能
    # %timeit, %who, %whos など
    squared = [n**2 for n in numbers]
    return squared

カスタムpdbコマンド作成

import pdb

class CustomPdb(pdb.Pdb):
    def do_list_vars(self, arg):
        """カスタムコマンド: 型別に変数一覧表示"""
        frame = self.curframe
        vars_by_type = {}
        
        for name, value in frame.f_locals.items():
            type_name = type(value).__name__
            if type_name not in vars_by_type:
                vars_by_type[type_name] = []
            vars_by_type[type_name].append((name, value))
        
        for type_name, vars_list in vars_by_type.items():
            print(f"\n{type_name}:")
            for name, value in vars_list:
                print(f"  {name} = {repr(value)}")
    
    def do_memory_usage(self, arg):
        """カスタムコマンド: メモリ使用量表示"""
        import sys
        frame = self.curframe
        
        print("Variable memory usage:")
        for name, value in frame.f_locals.items():
            size = sys.getsizeof(value)
            print(f"  {name}: {size} bytes")

# カスタムデバッガーの使用
def debug_with_custom_commands():
    numbers = list(range(1000))
    text = "Hello, World!" * 100
    data_dict = {f"key_{i}": i for i in range(50)}
    
    # カスタムpdbインスタンス作成
    debugger = CustomPdb()
    debugger.set_trace()
    
    # カスタムコマンドが利用可能:
    # (CustomPdb) list_vars     # 型別変数一覧
    # (CustomPdb) memory_usage  # メモリ使用量
    
    result = len(numbers) + len(text) + len(data_dict)
    return result