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
-
shimizukawaさんに教えてもらった https://mobile.twitter.com/shimizukawa/status/1326498954574356481↩
-
この3つの引数は context managerを作るときの
__exit__()
などでもおなじみ↩ -
実装を考えると、クックブックのコードよりは、tracebackを直接渡すようなコードにしたほうがきれいな気もする。↩
-
コレを悪用すると、withの中を評価せずに、ソースコードを取り出し、変換を加えてexecするみたいな事もできたりする。 https://gist.github.com/podhmo/b2aaf26e14999c871509783fe8f55909↩