pythonでbreakpointなしに例外の発生元で自動的にpdbする方法のメモ

デバッガーを使いたいがコードにbreakpoint を仕込むのが面倒な時がある。

例えば以下のようなコードがあるとして、 foo > bar > boo と辿っていた中でのbooでpdbを実行して欲しい。

def foo():
    print("foo")
    bar()
    print("foo")


def bar():
    print("bar")
    boo()
    print("bar")


def boo():
    print("boo")
    raise Exception("oops") # ここでpdbを

foo()

oopsの部分に、breakpointなしで、pdbしたい。

回答

正解から言うと、以下のような関数を作れば良いらしい1

すごく古いオライリーのクックブックに載っていた(https://www.oreilly.com/library/view/python-cookbook/0596001673/ch14s06.html)。

def info(type, value, tb):
    if hasattr(sys, "ps1") or not sys.stderr.isatty():
        # You are in interactive mode or don't have a tty-like
        # device, so call the default hook
        sys.__excepthook__(type, value, tb)
    else:
        import traceback, pdb

        # You are NOT in interactive mode; print the exception...
        traceback.print_exception(type, value, tb)
        # ...then start the debugger in post-mortem mode
        pdb.pm()


sys.excepthook = info

intractive shellなどへの対応を無視しちゃうと実質これだけ。

import traceback, pdb


def info(type, value, tb):
    pdb.pm()


sys.excepthook = info

詳細

以下詳細。キモは sys.excepthook による補足と、 pdbのpost moterm mode。

sys.excepthook

sys.excepthookが使える。

例外が発生し、その例外が捕捉されない場合、インタプリタは例外クラス・例外インスタンス・トレースバックオブジェクトを引数として sys.excepthook を呼び出します。対話セッション中に発生した場合はプロンプトに戻る直前に呼び出され、Pythonプログラムの実行中に発生した場合はプログラムの終了直前に呼び出されます。このトップレベルでの例外情報出力処理をカスタマイズする場合、 sys.excepthook に引数の数が三つの関数を指定します。

まぁ要は以下のようなsignatureの関数を受け取って、補足されない例外に対してこのhookが効くということのようだ2

def hook(typ, value, tb):
    ...

このhookに pdbをうまくしかけられると良い。

pdb.postmortem

実は、pdb.post_mortem()というメソッドがあることを知らなかったのだけれど、コレはその名の通り事後の解析をするためのメソッドのようだ。

内部的にはtracebackオブジェクトを渡せばそれが使われるし、そうでない場合は、sys.exc_infoから現在補足している例外を取り出して利用する。実際実装は以下のようになっていた3

def post_mortem(t=None):
    # handling the default
    if t is None:
        # sys.exc_info() returns (type, value, traceback) if an exception is
        # being handled, otherwise it returns None
        t = sys.exc_info()[2]
    if t is None:
        raise ValueError("A valid traceback must be passed if no "
                         "exception is being handled")

    p = Pdb()
    p.reset()
    p.interaction(None, t)

def pm():
    post_mortem(sys.last_traceback)

(実は、reset()や諸々でどのようにpdbが動いているかを知ろうとすると結構面白かったりする。内部的には bdbモジュールが使われている4)

実際しっかり止まる

$ python 03*.py
foo
bar
boo
Traceback (most recent call last):
  File "03pdb-on-exception.py", line 37, in <module>
    foo()
  File "03pdb-on-exception.py", line 6, in foo
    bar()
  File "03pdb-on-exception.py", line 12, in bar
    boo()
  File "03pdb-on-exception.py", line 18, in boo
    raise Exception("oops")
Exception: oops
> 03pdb-on-exception.py(18)boo()
-> raise Exception("oops")
(Pdb) q

素晴らしいですね。

gist


  1. shimizukawaさんに教えてもらった https://mobile.twitter.com/shimizukawa/status/1326498954574356481

  2. この3つの引数は context managerを作るときの __exit__() などでもおなじみ

  3. 実装を考えると、クックブックのコードよりは、tracebackを直接渡すようなコードにしたほうがきれいな気もする。

  4. コレを悪用すると、withの中を評価せずに、ソースコードを取り出し、変換を加えてexecするみたいな事もできたりする。 https://gist.github.com/podhmo/b2aaf26e14999c871509783fe8f55909