2012年10月26日

Python で標準添付の Debuggerのpdb を利用してデバッグするメモ

概要

pdb を利用してみる。

ドキュメント

公式ドキュメント「pdb — The Python Debugger」に概要が書いてある。

使い方

スクリプトを直接起動してデバッグする方法と、インタラクティブshell で起動する方法があるが、ここでは、スクリプトを起動してデバッグする方法で書く。
以下がデバッグするスクリプトのサンプル「even.py」。
自分の場合、スクリプトを直接デバッグすることがあまりなく、デバッグするのはライブラリとかをunittest経由でデバッグすることがが多いのでサンプルは unittest で書いている。
以下の例はクラス内の関数が偶数だけ返す所でバグがあり、奇数を返すようになっている。そんなに良い例ではないかも。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest


class Sample(object):
    """
    テスト対象のクラス
    """
    def to_even(self, lst):
        """
        リストを渡すと、偶数だけにして返す
        """

        # 意図的にバグらせていて、奇数しか返さない
        evenlst = [v for v in lst if v % 2 == 1]
        return evenlst


class TestSample(unittest.TestCase):
    """
    テストクラス
    """

    def test_main(self):
        clazz = Sample()

        #  0 から 10 のリストを生成 [0, 1, 2, .....]
        lst = xrange(10)
        evenlst = clazz.to_even(lst)

        # 偶数かテストする
        for item in evenlst:
            num = item % 2
            self.assertEqual(0, num)


if __name__ == '__main__':
    unittest.main()

とりあえず実行してみると以下のようにエラーになる。

> python even.py 
F
======================================================================
FAIL: test_main (__main__.TestSample)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "even.py", line 33, in test_main
    self.assertEqual(0, num)
AssertionError: 0 != 1

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

pdbを起動してみる。起動は以下のようにする。

python -m pdb even.py

起動すると pdb プロンプトが起動する。

> /path/to/even.py(3)<module>()
-> import unittest
(Pdb) 

上記の出力から「3行目」の「import」で実行が停止しているのがわかる。
「list」で停止位置の周辺ソースが表示される。

(Pdb) list
  1     #!/usr/bin/env python
  2     # -*- coding: utf-8 -*-
  3  -> import unittest
  4  
  5  
  6     class Sample(object):
  7         """
  8         テスト対象のクラス
  9         """
 10         def to_even(self, lst):
 11             """
(Pdb) 

行数とともに、現在の停止場所に「->」がある。

「list」のようにデバッグに利用できるコマンドの主な物は以下になる。コマンドには省略形が存在する物もある。

コマンド 省略形 概要
help h コマンドのヘルプを表示。
「help」でコマンド一覧、
「help コマンド名」でコマンドのヘルプ
が表示される。
where w スタックトレースを表示
down d スタックトレース中に1レベル下げる
up u スタックトレース中に1レベル上げる
step s 現在行を実行し、次の行に進む
次の行が関数の場合、関数内で停止
next n 現在行を実行し、次の行に進む
次の行が関数でも、関数内では停止しない
return r 現在の関数から抜ける
list l 現在行周辺のソースを表示
デフォルトは11行表示する
args a 現在関数の引数一覧を表示
p 式 式内容を表示
pp 式 式内容を pprint で表示
quit q デバッガ終了

現在行が「import」なので「n(ext)」で進める。
間違えて「s(tep)」すると「import」を実行する Python のライブラリ内に入る。その場合は「r(eturn)」とかすると戻ってこれる。

(Pdb) n
> /path/to/even.py(6)<module>()
-> class Sample(object):
(Pdb) 

class の行になるが、実行されるわけではない。
再度「n」するが、この「n」は一度実行すると次回は「リターンキー」で同じ事を実行する。

(Pdb) 
> /path/to/even.py(18)<module>()
-> class TestSample(unittest.TestCase):
(Pdb) 

「c(ontinue)」すると、停止するまで実行する。

(Pdb) c
F
======================================================================
FAIL: test_main (__main__.TestSample)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "even.py", line 33, in test_main
    self.assertEqual(0, num)
AssertionError: 0 != 1

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
The program exited via sys.exit(). Exit status:  True
> /path/to/even.py(3)<module>()
-> import unittest

ブレークポイント

通常 step 実行とかしないで、ブレークポイントを利用する。ブレークポイント関連のコマンドは以下。

コマンド 省略形 概要
break 行数 or 関数名 b 指定行数にブレークポイントを付ける
tbreak 行数 or 関数名 一時的ブレークポイントを付ける
一回通過すると消える
clear 行数 or bp番号 cl ブレークポイントを削除
disable bp番号 ブレークポイントを停止
enable bp番号 ブレークポイントを有効化
ignore bp番号 回数 ブレークポイントを回数分無視する
condition bp番号 ブレークポイントの状態を確認
commands bp番号 ブレークポイントを表示
continue c ブレークポイントで停止してる場合、実行を継続

「33行目」にブレークポイントを設定し「c」して「l」で確認してみる。

(Pdb) b 33
Breakpoint 1 at /path/to/even.py:33
(Pdb) c
> /path/to/even.py(33)test_main()
-> for item in evenlst:
(Pdb) l
 28             #  0 から 10 のリストを生成 [0, 1, 2, .....]
 29             lst = xrange(10)
 30             evenlst = clazz.to_even(lst)
 31  
 32             # 偶数かテストする
 33 B->         for item in evenlst:
 34                 num = item % 2
 35                 self.assertEqual(0, num)
 36  
 37  
 38     if __name__ == '__main__':
(Pdb) 

ここで「evenlst」の内容を表示してみる。

(Pdb) p evenlst
[1, 3, 5, 7, 9]
(Pdb) 

バグっている事がわかる。

「n」を2回して「l」して「p」してみる。「p」で複数表示の場合はカンマで区切る。

(Pdb) p item,num
(1, 1)
(Pdb) 

バグ原因が判明したら「q」などで終了すれば良い。

まとめ

他者の作成したライブラリ等はソース修正するとまずいので、 print を入れることができない場合が多く、そういう場合は pdb は便利。
本当はライブラリ内で Exception が発生したりするので、結構デバッグは大変だったりする。
自作のスクリプトの場合は、普通は pdb を使うよりも、いかに良くテストを書くかの方が重要な気がしている。pdbを使わなければいけないのはテストが下手だと思った方が良い。

blog comments powered by Disqus