Pdbを使用したPythonデバッグ

Pdbを使用したPythonデバッグ

アプリケーションのデバッグは、望ましくないアクティビティになる場合があります。 あなたは時間の制約の下で仕事をするのに忙しく、ただそれを機能させたいだけです。 ただし、新しい言語機能を学習したり、新しいアプローチを試したりして、何かがどのように機能しているかをより深く理解したい場合もあります。

状況に関係なく、コードのデバッグは必要なので、デバッガで快適に作業することをお勧めします。 このチュートリアルでは、Pythonのインタラクティブなソースコードデバッガーであるpdbの基本的な使い方を紹介します。

pdbの一般的な使用方法をいくつか紹介します。 このチュートリアルをブックマークしておくと、後で必要になったときにすぐに参照できます。 pdb、およびその他のデバッガーは不可欠なツールです。 デバッガが必要なときは、代用品はありません。 本当に必要です。

このチュートリアルの終わりまでに、デバッガーを使用してアプリケーション内の変数の状態を確認する方法がわかります。 また、アプリケーションの実行フローをいつでも停止および再開できるため、コードの各行が内部状態にどのように影響するかを正確に確認できます。

これは、見つけにくいバグを追跡するのに最適で、障害のあるコードをより迅速かつ確実に修正できます。 pdbのコードをステップ実行し、値がどのように変化するかを確認することは、実際の目を見張るものであり、時々「顔の掌」とともに「あは」の瞬間につながります。

pdbはPythonの標準ライブラリの一部であるため、常にそこにあり、使用できます。 使い慣れたGUIデバッガーにアクセスできない環境でコードをデバッグする必要がある場合、これは命を救うことができます。

このチュートリアルのサンプルコードでは、Python 3.6を使用しています。 これらの例のソースコードはGitHubにあります。

このチュートリアルの最後に、Essential pdb Commandsのクイックリファレンスがあります。

デバッグ中にチートシートとして使用できる印刷可能なpdbコマンドリファレンスもあります。

Free Bonus:Click here to get a printable "pdb Command Reference" (PDF)は机の上に置いておき、デバッグ中に参照できます。

はじめに:変数の値を印刷する

この最初の例では、変数の値をチェックする最も単純な形式でpdbを使用する方法を見ていきます。

デバッガーに侵入する場所に次のコードを挿入します。

import pdb; pdb.set_trace()

上記の行が実行されると、Pythonは停止し、次に何をすべきかを指示するのを待ちます。 (Pdb)プロンプトが表示されます。 これは、インタラクティブデバッガーで一時停止し、コマンドを入力できることを意味します。

Python 3.7以降、there’s another way to enter the debuggerPEP 553は、組み込み関数breakpoint()を記述します。これにより、デバッガーへの入力が簡単で一貫性があります。

breakpoint()

デフォルトでは、上記のように、breakpoint()pdbをインポートし、pdb.set_trace()を呼び出します。 ただし、breakpoint()の使用はより柔軟であり、APIおよび環境変数PYTHONBREAKPOINTの使用を介してデバッグ動作を制御できます。 たとえば、環境でPYTHONBREAKPOINT=0を設定すると、breakpoint()が完全に無効になり、デバッグが無効になります。 Python 3.7以降を使用している場合は、pdb.set_trace()の代わりにbreakpoint()を使用することをお勧めします。

コマンドラインから直接Pythonを実行し、オプション-m pdbを渡すことで、ソースを変更せずにpdb.set_trace()またはbreakpoint()を使用せずにデバッガーに侵入することもできます。 アプリケーションがコマンドライン引数を受け入れる場合、ファイル名の後に通常どおりにそれらを渡します。 例えば:

$ python3 -m pdb app.py arg1 arg2

多くのpdbコマンドが利用可能です。 このチュートリアルの最後に、Essential pdb Commandsのリストがあります。 とりあえず、pコマンドを使用して変数の値を出力しましょう。 (Pdb)プロンプトでp variable_nameと入力して、その値を出力します。

例を見てみましょう。 example1.pyのソースは次のとおりです。

#!/usr/bin/env python3

filename = __file__
import pdb; pdb.set_trace()
print(f'path = {filename}')

これをシェルから実行すると、次の出力が得られます。

$ ./example1.py
> /code/example1.py(5)()
-> print(f'path = {filename}')
(Pdb)

コマンドラインからサンプルや独自のコードを実行するのに問題がある場合は、How Do I Make My Own Command-Line Commands Using Python?をお読みください。Windowsを使用している場合は、Python Windows FAQを確認してください。

ここで、p filenameと入力します。 君は見るべきだ:

(Pdb) p filename
'./example1.py'
(Pdb)

シェルでCLI(コマンドラインインターフェイス)を使用しているため、文字と書式に注意してください。 必要なコンテキストを提供します:

  • >は最初の行を開始し、現在のソースファイルを示します。 ファイル名の後に、括弧内に現在の行番号があります。 次は関数の名前です。 この例では、関数内およびモジュールレベルで一時停止されていないため、<module>()が表示されます。

  • ->は2行目を開始し、Pythonが一時停止されている現在のソース行です。 この行はまだ実行されていません。 この例では、これは上記の>行からのexample1.pyの行5です。

  • (Pdb)はpdbのプロンプトです。 コマンドを待っています。

コマンドqを使用して、デバッグを終了し、終了します。

式を印刷する

印刷コマンドpを使用する場合、Pythonで評価される式を渡します。 変数名を渡すと、pdbは現在の値を出力します。 ただし、実行中のアプリケーションの状態を調査するには、さらに多くのことができます。

この例では、関数get_path()が呼び出されます。 この関数で何が起こっているかを調べるために、pdb.set_trace()の呼び出しを挿入して、実行が戻る直前に実行を一時停止しました。

#!/usr/bin/env python3

import os


def get_path(filename):
    """Return file's path or empty string if no path."""
    head, tail = os.path.split(filename)
    import pdb; pdb.set_trace()
    return head


filename = __file__
print(f'path = {get_path(filename)}')

これをシェルから実行すると、出力が得られます:

$ ./example2.py
> /code/example2.py(10)get_path()
-> return head
(Pdb)

ここはどこ?

  • >:関数get_path()の行10のソースファイルexample2.pyにいます。 これは、pコマンドが変数名を解決するために使用する参照フレームです。 現在のスコープまたはコンテキスト。

  • ->:実行はreturn headで一時停止しました。 この行はまだ実行されていません。 これは、上記の>行からの関数get_path()example2.pyの行10です。

いくつかの式を印刷して、アプリケーションの現在の状態を見てみましょう。 最初にコマンドll(longlist)を使用して、関数のソースを一覧表示します。

(Pdb) ll
  6     def get_path(filename):
  7         """Return file's path or empty string if no path."""
  8         head, tail = os.path.split(filename)
  9         import pdb; pdb.set_trace()
 10  ->     return head
(Pdb) p filename
'./example2.py'
(Pdb) p head, tail
('.', 'example2.py')
(Pdb) p 'filename: ' + filename
'filename: ./example2.py'
(Pdb) p get_path

(Pdb) p getattr(get_path, '__doc__')
"Return file's path or empty string if no path."
(Pdb) p [os.path.split(p)[1] for p in os.path.sys.path]
['pdb-basics', 'python36.zip', 'python3.6', 'lib-dynload', 'site-packages']
(Pdb)

評価のために、有効なPython式をpに渡すことができます。

これは、デバッグ中に、実行時にアプリケーションで代替実装を直接テストする場合に特に役立ちます。

コマンドpp(プリティプリント)を使用して、式をプリティプリントすることもできます。 これは、大量の出力を持つ変数または式を印刷する場合に役立ちます。 リストと辞書。 きれいに印刷すると、オブジェクトが許容される幅に収まらない場合はオブジェクトを1行に保持したり、複数行に分割したりできます。

コードのステップ実行

デバッグ時にコードをステップ実行するために使用できる2つのコマンドがあります。

コマンド 説明

n(次)

現在の関数の次の行に到達するか、戻るまで実行を続けます。

s(ステップ)

現在の行を実行し、最初の可能な機会に停止します(呼び出された関数または現在の関数のいずれかで)。

untという名前の3番目のコマンドがあります(まで)。 n(次)に関連しています。 これについては、このチュートリアルの後半のセクションContinuing Executionで説明します。

n(次)とs(ステップ)の違いは、pdbが停止する場所です。

n(next)を使用して、次の行まで実行を継続し、現在の関数内にとどまります。 外部関数が呼び出されても停止しません。 次に、「ローカルに滞在する」または「ステップオーバー」と考えてください。

s(ステップ)を使用して現在の行を実行し、外部関数が呼び出された場合は停止します。 ステップを「ステップイン」と考えてください。 別の関数で実行が停止された場合、s--Call--を出力します。

nsはどちらも、現在の関数の終わりに達すると実行を停止し、->の後の次の行の終わりに戻り値とともに--Return--を出力します。

両方のコマンドを使用した例を見てみましょう。 example3.pyのソースは次のとおりです。

#!/usr/bin/env python3

import os


def get_path(filename):
    """Return file's path or empty string if no path."""
    head, tail = os.path.split(filename)
    return head


filename = __file__
import pdb; pdb.set_trace()
filename_path = get_path(filename)
print(f'path = {filename_path}')

これをシェルから実行してnと入力すると、次の出力が得られます。

$ ./example3.py
> /code/example3.py(14)()
-> filename_path = get_path(filename)
(Pdb) n
> /code/example3.py(15)()
-> print(f'path = {filename_path}')
(Pdb)

n(next)を使用して、次の行である15行で停止しました。 <module>()で「ローカルにとどまり」、get_path()への呼び出しを「ステップオーバー」しました。 現在モジュールレベルであり、別の関数内で一時停止していないため、関数は<module>()です。

sを試してみましょう:

$ ./example3.py
> /code/example3.py(14)()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/example3.py(6)get_path()
-> def get_path(filename):
(Pdb)

s(ステップ)では、関数get_path()で行14で呼び出されたため、行6で停止しました。 sコマンドの後の行--Call--に注意してください。

便利なことに、pdbは最後のコマンドを記憶しています。 多くのコードをステップ実行している場合は、[.kbd .key-enter]#Enter#を押すだけで最後のコマンドを繰り返すことができます。

以下は、snの両方を使用してコードをステップ実行する例です。 関数get_path()に「ステップイン」して停止したいので、最初にsを入力します。 次に、nを1回入力して、他の関数呼び出しを「ローカルに維持」または「ステップオーバー」し、[.kbd .key-enter]#Enter#を押して、最後のソース行に到達するまでnコマンドを繰り返します。

$ ./example3.py
> /code/example3.py(14)()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/example3.py(6)get_path()
-> def get_path(filename):
(Pdb) n
> /code/example3.py(8)get_path()
-> head, tail = os.path.split(filename)
(Pdb)
> /code/example3.py(9)get_path()
-> return head
(Pdb)
--Return--
> /code/example3.py(9)get_path()->'.'
-> return head
(Pdb)
> /code/example3.py(15)()
-> print(f'path = {filename_path}')
(Pdb)
path = .
--Return--
> /code/example3.py(15)()->None
-> print(f'path = {filename_path}')
(Pdb)

--Call----Return--に注意してください。 これは、実行が停止した理由を知らせるpdbです。 n(次へ)とs(ステップ)は、関数が戻る前に停止します。 そのため、上記の--Return--行が表示されます。

上記の最初の--Return--の後の行末の->'.'にも注意してください。

--Return--
> /code/example3.py(9)get_path()->'.'
-> return head
(Pdb)

戻る前に関数の最後でpdbが停止すると、戻り値も出力されます。 この例では、'.'です。

ソースコードのリスト

コマンドllを忘れないでください(longlist:現在の関数またはフレームのソースコード全体を一覧表示します)。 なじみのないコードをステップスルーする場合や、コンテキスト全体の機能全体を表示する場合に非常に役立ちます。

例を示しましょう。

$ ./example3.py
> /code/example3.py(14)()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/example3.py(6)get_path()
-> def get_path(filename):
(Pdb) ll
  6  -> def get_path(filename):
  7         """Return file's path or empty string if no path."""
  8         head, tail = os.path.split(filename)
  9         return head
(Pdb)

コードの短いスニペットを表示するには、コマンドl(list)を使用します。 引数なしで、現在の行の周りに11行を出力するか、前のリストを継続します。 引数.を渡して、常に現在の行の周囲に11行をリストします。l .

$ ./example3.py
> /code/example3.py(14)()
-> filename_path = get_path(filename)
(Pdb) l
  9         return head
 10
 11
 12     filename = __file__
 13     import pdb; pdb.set_trace()
 14  -> filename_path = get_path(filename)
 15     print(f'path = {filename_path}')
[EOF]
(Pdb) l
[EOF]
(Pdb) l .
  9         return head
 10
 11
 12     filename = __file__
 13     import pdb; pdb.set_trace()
 14  -> filename_path = get_path(filename)
 15     print(f'path = {filename_path}')
[EOF]
(Pdb)

ブレークポイントを使用する

ブレークポイントは非常に便利で、時間を大幅に節約できます。 興味のない数十行をステップスルーする代わりに、調査したい場所にブレークポイントを作成するだけです。 必要に応じて、特定の条件が真の場合にのみ中断するようにpdbに指示することもできます。

コマンドb(break)を使用して、ブレークポイントを設定します。 実行を停止する行番号または関数名を指定できます。

breakの構文は次のとおりです。

b(reak) [ ([filename:]lineno | function) [, condition] ]

行番号linenoの前にfilename:が指定されていない場合は、現在のソースファイルが使用されます。

bのオプションの第2引数:conditionに注意してください。 これはとても強力です。 特定の条件が存在する場合にのみ中断したい状況を想像してください。 Pythonの式を2番目の引数として渡すと、式がtrueと評価されたときにpdbが壊れます。 以下の例でこれを行います。

この例では、ユーティリティモジュールutil.pyがあります。 関数get_path()で実行を停止するブレークポイントを設定しましょう。

メインスクリプトexample4.pyのソースは次のとおりです。

#!/usr/bin/env python3

import util

filename = __file__
import pdb; pdb.set_trace()
filename_path = util.get_path(filename)
print(f'path = {filename_path}')

ユーティリティモジュールutil.pyのソースは次のとおりです。

def get_path(filename):
    """Return file's path or empty string if no path."""
    import os
    head, tail = os.path.split(filename)
    return head

まず、ソースファイル名と行番号を使用してブレークポイントを設定しましょう。

$ ./example4.py
> /code/example4.py(7)()
-> filename_path = util.get_path(filename)
(Pdb) b util:5
Breakpoint 1 at /code/util.py:5
(Pdb) c
> /code/util.py(5)get_path()
-> return head
(Pdb) p filename, head, tail
('./example4.py', '.', 'example4.py')
(Pdb)

コマンドc(continue)は、ブレークポイントが見つかるまで実行を継続します。

次に、関数名を使用してブレークポイントを設定しましょう。

$ ./example4.py
> /code/example4.py(7)()
-> filename_path = util.get_path(filename)
(Pdb) b util.get_path
Breakpoint 1 at /code/util.py:1
(Pdb) c
> /code/util.py(3)get_path()
-> import os
(Pdb) p filename
'./example4.py'
(Pdb)

すべてのブレークポイントのリストを表示するには、引数なしでbを入力します。

(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /code/util.py:1
(Pdb)

コマンドdisable bpnumberおよびenable bpnumberを使用して、ブレークポイントを無効にしたり、再度有効にしたりできます。 bpnumberは、ブレークポイントリストの最初の列Numからのブレークポイント番号です。 Enb列の値の変更に注意してください。

(Pdb) disable 1
Disabled breakpoint 1 at /code/util.py:1
(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep no    at /code/util.py:1
(Pdb) enable 1
Enabled breakpoint 1 at /code/util.py:1
(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /code/util.py:1
(Pdb)

ブレークポイントを削除するには、コマンドcl(clear)を使用します。

cl(ear) filename:lineno
cl(ear) [bpnumber [bpnumber...]]

次に、Python式を使用してブレークポイントを設定しましょう。 問題のある機能が特定の入力を受け取った場合にのみ中断したい状況を想像してください。

このシナリオ例では、get_path()関数は、相対パスを受信すると失敗します。 ファイルのパスが/で始まっていません。 この場合、trueと評価される式を作成し、2番目の引数としてbに渡します。

$ ./example4.py
> /code/example4.py(7)()
-> filename_path = util.get_path(filename)
(Pdb) b util.get_path, not filename.startswith('/')
Breakpoint 1 at /code/util.py:1
(Pdb) c
> /code/util.py(3)get_path()
-> import os
(Pdb) a
filename = './example4.py'
(Pdb)

上記のブレークポイントを作成し、cを入力して実行を続行すると、式がtrueと評価されたときにpdbが停止します。 コマンドa(args)は、現在の関数の引数リストを出力します。

上記の例で、行番号ではなく関数名を使用してブレークポイントを設定する場合、式では関数の入力時に使用可能な関数引数またはグローバル変数のみを使用する必要があることに注意してください。 それ以外の場合、ブレークポイントは、式の値に関係なく、関数の実行を停止します。

関数内にある変数名を持つ式を使用してブレークする必要がある場合、つまり 関数の引数リストにない変数名、行番号を指定します:

$ ./example4.py
> /code/example4.py(7)()
-> filename_path = util.get_path(filename)
(Pdb) b util:5, not head.startswith('/')
Breakpoint 1 at /code/util.py:5
(Pdb) c
> /code/util.py(5)get_path()
-> return head
(Pdb) p head
'.'
(Pdb) a
filename = './example4.py'
(Pdb)

コマンドtbreakを使用して、一時的なブレークポイントを設定することもできます。 最初にヒットすると自動的に削除されます。 bと同じ引数を使用します。

継続的な実行

これまで、n(次)とs(ステップ)でコードをステップスルーし、b(ブレーク)とc(続行)でブレークポイントを使用することを見てきました。 。

関連するコマンドもあります:unt(まで)。

untを使用して、cと同様に実行を続行しますが、現在の行よりも大きい次の行で停止します。 untの方が便利で使いやすく、まさにあなたが望むものである場合があります。 これを以下の例で示します。

まず、untの構文と説明を見てみましょう。

コマンド 構文 説明

unt

unt(il)[lineno]

linenoがない場合は、現在の行よりも大きい数の行に到達するまで実行を続けます。 linenoを使用して、それ以上の数の行に到達するまで実行を続行します。 どちらの場合も、現在のフレームが戻ったときに停止します。

行番号引数linenoを渡すかどうかに応じて、untは次の2つの方法で動作します。

  • linenoがない場合は、現在の行よりも大きい数の行に到達するまで実行を続けます。 これはn(次)に似ています。 これは、コードを実行して「ステップオーバー」する別の方法です。 nuntの違いは、untは、現在の行よりも大きい数の行に到達した場合にのみ停止することです。 nは、論理的に実行された次の行で停止します。

  • linenoを使用して、それ以上の数の行に到達するまで実行を続行します。 これは、行番号引数を持つc(続行)のようなものです。

どちらの場合も、n(次へ)やs(ステップ)と同様に、現在のフレーム(関数)が戻るとuntが停止します。

untで注意すべき主な動作は、現在の行または指定された行の行番号greater or equalに達すると停止することです。

実行を継続し、現在のソースファイルのさらに下で停止する場合は、untを使用します。 行番号引数を渡すかどうかに応じて、n(次)とb(ブレーク)のハイブリッドのように扱うことができます。

以下の例には、ループのある関数があります。 ここでは、ループの各反復をステップ実行したり、ブレークポイントを設定したりせずに、コードの実行を継続し、ループの後に停止する必要があります。

example4unt.pyのソースの例は次のとおりです。

#!/usr/bin/env python3

import os


def get_path(fname):
    """Return file's path or empty string if no path."""
    import pdb; pdb.set_trace()
    head, tail = os.path.split(fname)
    for char in tail:
        pass  # Check filename char
    return head


filename = __file__
filename_path = get_path(filename)
print(f'path = {filename_path}')

そして、untを使用したコンソール出力:

$ ./example4unt.py
> /code/example4unt.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) unt
> /code/example4unt.py(10)get_path()
-> for char in tail:
(Pdb)
> /code/example4unt.py(11)get_path()
-> pass  # Check filename char
(Pdb)
> /code/example4unt.py(12)get_path()
-> return head
(Pdb) p char, tail
('y', 'example4unt.py')

llコマンドを最初に使用して関数のソースを出力し、次にuntを使用しました。 pdbは最後に入力されたコマンドを記憶しているので、[.kbd .key-enter]#Enter#を押してuntコマンドを繰り返しました。 これは、現在の行よりも大きいソース行に到達するまで、コード全体で実行を継続しました。

上記のコンソール出力で、pdbが行10および11で1回だけ停止したことに注意してください。 untが使用されたため、ループの最初の反復でのみ実行が停止されました。 ただし、ループの各反復が実行されました。 これは、出力の最後の行で確認できます。 char変数の値'y'は、tailの値'example4unt.py'の最後の文字と同じです。

式を表示する

pおよびppを使用して式を出力するのと同様に、コマンドdisplay [expression]を使用して、実行が停止したときに式の値が変更された場合に自動的に表示するようにpdbに指示できます。 コマンドundisplay [expression]を使用して、表示式をクリアします。

両方のコマンドの構文と説明は次のとおりです。

コマンド 構文 説明

display

表示[式]

現在のフレームで実行が停止するたびに、expressionの値が変更された場合はその値を表示します。 expressionがない場合、現在のフレームのすべての表示式をリストします。

undisplay

undisplay [式]

現在のフレームにexpressionを表示しなくなります。 expressionがない場合、現在のフレームのすべての表示式をクリアします。

以下は、example4display.pyの例であり、ループでの使用を示しています。

$ ./example4display.py
> /code/example4display.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) b 11
Breakpoint 1 at /code/example4display.py:11
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
(Pdb) display char
display char: 'e'
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'x'  [old: 'e']
(Pdb)
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'a'  [old: 'x']
(Pdb)
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'm'  [old: 'a']

上記の出力では、ブレークポイントに到達するたびにその値が変更されていたため、pdbはchar変数の値を自動的に表示しました。 これが役立つ場合がありますが、displayを使用する別の方法があります。

displayを複数回入力して、式のウォッチリストを作成できます。 これは、pよりも使いやすい場合があります。 関心のあるすべての式を追加したら、displayと入力するだけで、現在の値を確認できます。

$ ./example4display.py
> /code/example4display.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) b 11
Breakpoint 1 at /code/example4display.py:11
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
(Pdb) display char
display char: 'e'
(Pdb) display fname
display fname: './example4display.py'
(Pdb) display head
display head: '.'
(Pdb) display tail
display tail: 'example4display.py'
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'x'  [old: 'e']
(Pdb) display
Currently displaying:
char: 'x'
fname: './example4display.py'
head: '.'
tail: 'example4display.py'

Pythonの発信者ID

この最後のセクションでは、これまでに学んだことを基に、素晴らしい成果を上げます。 電話システムの発信者識別機能に関して「発信者ID」という名前を使用します。 Pythonに適用されることを除いて、これはまさにこの例が示すものです。

メインスクリプトexample5.pyのソースは次のとおりです。

#!/usr/bin/env python3

import fileutil


def get_file_info(full_fname):
    file_path = fileutil.get_path(full_fname)
    return file_path


filename = __file__
filename_path = get_file_info(filename)
print(f'path = {filename_path}')

ユーティリティモジュールfileutil.pyは次のとおりです。

def get_path(fname):
    """Return file's path or empty string if no path."""
    import os
    import pdb; pdb.set_trace()
    head, tail = os.path.split(fname)
    return head

このシナリオでは、ユーティリティモジュールの関数get_path()を含む大規模なコードベースがあり、無効な入力で呼び出されていると想像してください。 ただし、異なるパッケージの多くの場所から呼び出されています。

発信者をどのように見つけますか?

コマンドw(where)を使用して、最新のフレームを下部にしたスタックトレースを出力します。

$ ./example5.py
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) w
  /code/example5.py(12)()
-> filename_path = get_file_info(filename)
  /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb)

紛らわしい場合や、スタックトレースやフレームが不明な場合でも心配しないでください。 これらの用語を以下に説明します。 聞こえるほど難しくはありません。

最新のフレームが一番下にあるので、そこから始めて、下から順に読んでください。 ->で始まる行を見てください。ただし、関数get_path()にpdbを入力するためにpdb.set_trace()が使用されたため、最初のインスタンスはスキップしてください。 この例では、関数get_path()を呼び出したソース行は次のとおりです。

-> file_path = fileutil.get_path(full_fname)

->の上の行には、ファイル名、行番号(括弧内)、およびソース行が含まれる関数名が含まれています。 したがって、呼び出し元は次のとおりです。

  /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)

デモンストレーション用のこの小さな例では驚くことではありませんが、悪い入力値の発生元を特定するための条件でブレークポイントを設定した大きなアプリケーションを想像してください。

これで、呼び出し元を見つける方法がわかりました。

しかし、このスタックトレースとフレームのものはどうですか?

スタックトレースは、関数呼び出しを追跡するためにPythonが作成したすべてのフレームの単なるリストです。 フレームは、Pythonが関数が呼び出されたときに作成され、戻るときに削除されるデータ構造です。 スタックは、任意の時点でのフレームまたは関数呼び出しの順序付きリストです。 (関数呼び出し)スタックは、関数が呼び出されてから戻るときに、アプリケーションのライフサイクル全体にわたって拡大および縮小します。

印刷されたとき、このフレームの順序付きリスト、スタックはスタックトレースと呼ばれます。 上記で呼び出し元を見つけるために行ったように、コマンドwを入力すると、いつでも表示できます。

詳細については、このcall stack article on Wikipediaを参照してください。

pdbをよりよく理解し、より多くを引き出すために、wのヘルプを詳しく見てみましょう。

(Pdb) h w
w(here)
        Print a stack trace, with the most recent frame at the bottom.
        An arrow indicates the "current frame", which determines the
        context of most commands. 'bt' is an alias for this command.

pdbは「現在のフレーム」とはどういう意味ですか?

現在のフレームは、pdbが実行を停止した現在の関数と考えてください。 つまり、現在のフレームは、アプリケーションが現在一時停止されている場所であり、p(印刷)などのpdbコマンドの参照の「フレーム」として使用されます。

pおよびその他のコマンドは、必要に応じてコンテキストに現在のフレームを使用します。 pの場合、現在のフレームが変数参照の検索と印刷に使用されます。

pdbがスタックトレースを出力するとき、矢印>は現在のフレームを示します。

これはどのように便利ですか?

2つのコマンドu(上)とd(下)を使用して、現在のフレームを変更できます。 これをpと組み合わせると、任意のフレームの呼び出しスタックに沿った任意の時点で、アプリケーションの変数と状態を検査できます。

両方のコマンドの構文と説明は次のとおりです。

コマンド 構文 説明

u

u(p)[カウント]

スタックトレースで現在のフレームcount(デフォルトは1)レベルを上に移動します(古いフレームに)。

d

d(所有)[カウント]

スタックトレースで現在のフレームcount(デフォルトは1)レベルを下に移動します(新しいフレームに)。

uおよびdコマンドを使用した例を見てみましょう。 このシナリオでは、example5.pyの関数get_file_info()にローカルな変数full_fnameを検査します。 これを行うには、コマンドuを使用して現在のフレームを1レベル上に変更する必要があります。

$ ./example5.py
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) w
  /code/example5.py(12)()
-> filename_path = get_file_info(filename)
  /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) u
> /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
(Pdb) p full_fname
'./example5.py'
(Pdb) d
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) p fname
'./example5.py'
(Pdb)

pdb.set_trace()の呼び出しは、関数get_path()fileutil.pyにあるため、現在のフレームは最初にそこに設定されます。 上記の出力の1行目で確認できます。

> /code/fileutil.py(5)get_path()

example5.pyの関数get_file_info()のローカル変数full_fnameにアクセスして出力するには、コマンドuを使用して1つ上のレベルに移動しました。

(Pdb) u
> /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)

上記のuの出力では、pdbが1行目の先頭に矢印>を出力していることに注意してください。 これは、フレームが変更されたことを知らせるpdbであり、このソースの場所は現在のフレームです。 変数full_fnameにアクセスできるようになりました。 また、2行目の->で始まるソース行が実行されていることを認識することが重要です。 フレームがスタックの上位に移動されたため、fileutil.get_path()が呼び出されました。 uを使用して、スタックを(ある意味では、過去にさかのぼって)fileutil.get_path()が呼び出された関数example5.get_file_info()に移動しました。

例を続けると、full_fnameが出力された後、現在のフレームがdを使用して元の場所に移動され、get_path()のローカル変数fnameが出力されました。

必要に応じて、count引数をuまたはdに渡すことで、一度に複数のフレームを移動することもできます。 たとえば、u 2と入力して、example5.pyのモジュールレベルに移動できます。

$ ./example5.py
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) u 2
> /code/example5.py(12)()
-> filename_path = get_file_info(filename)
(Pdb) p filename
'./example5.py'
(Pdb)

デバッグしてさまざまなことを考えているときに、自分がどこにいるかを忘れがちです。 適切な名前のコマンドw(where)をいつでも使用して、実行が一時停止されている場所と現在のフレームを確認できることを覚えておいてください。

必須のpdbコマンド

pdbで少し時間を費やすと、少しの知識が大いに役立つことがわかります。 ヘルプは、hコマンドでいつでも利用できます。

hまたはhelp <topic>と入力するだけで、すべてのコマンドのリストを取得したり、特定のコマンドまたはトピックのヘルプを表示したりできます。

クイックリファレンスとして、重要なコマンドのリストを次に示します。

コマンド 説明

p

式の値を出力します。

pp

式の値をプリティプリントします。

n

現在の関数の次の行に到達するか、戻るまで実行を続けます。

s

現在の行を実行し、最初の可能な機会に停止します(呼び出された関数または現在の関数のいずれかで)。

c

実行を続行し、ブレークポイントが検出された場合にのみ停止します。

unt

現在の行よりも大きい番号の行に到達するまで実行を続けます。 行番号引数を使用して、それ以上の数の行に到達するまで実行を続行します。

l

現在のファイルのソースコードを一覧表示します。 引数なしで、現在の行の周りに11行をリストするか、前のリストを継続します。

ll

現在の関数またはフレームのソースコード全体を一覧表示します。

b

引数なしで、すべてのブレークをリストします。 行番号引数を使用して、現在のファイルのこの行にブレークポイントを設定します。

w

最新のフレームを一番下にして、スタックトレースを印刷します。 矢印は現在のフレームを示し、ほとんどのコマンドのコンテキストを決定します。

u

スタックトレースで現在のフレームカウント(デフォルトは1)レベルを上に移動します(古いフレームに)。

d

スタックトレースで現在のフレームカウント(デフォルトは1)レベルを下に移動します(新しいフレームに)。

h

使用可能なコマンドのリストを参照してください。

h

コマンドまたはトピックのヘルプを表示します。

h pdb

完全なpdbドキュメントを表示します。

q

デバッガーを終了して終了します。

pdbを使用したPythonデバッグ:結論

このチュートリアルでは、pdbのいくつかの基本的で一般的な使用法について説明しました。

  • 印刷式

  • n(次へ)とs(ステップ)でコードをステップスルーする

  • ブレークポイントを使用する

  • untで実行を継続する(まで)

  • 式を表示する

  • 関数の呼び出し元を見つける

お役に立てば幸いです。 詳細については、次をご覧ください。

例で使用されているソースコードは、関連するGitHub repositoryにあります。 デバッグ中にチートシートとして使用できる、印刷可能なpdbコマンドリファレンスを確認してください。

Free Bonus:Click here to get a printable "pdb Command Reference" (PDF)は机の上に置いておき、デバッグ中に参照できます。

また、GUIベースのPythonデバッガーを試してみたい場合は、Python IDEs and Editors Guideを読んで、どのオプションが最適かを確認してください。 ハッピーパイソン!