らくがきちょう

なんとなく ~所属組織/団体とは無関係であり、個人の見解です~

PySnooper を使って Python スクリプトをデバッグする

PySnooper を使うと print()pdb を使わずにデバッグを行うことが出来ます。 PySnooper のページには PySnooper is a poor man's debugger. と書かれていました。 PySnooper の使い方をメモしておきます。 尚、ソースコードcool-RR / PySnooper にありました。

ソースコードの行数

単純に行数だけ数えた場合、現時点では約 800 行のようです。 熟読出来ていませんが、中核となる実装は tracer.py に集中しているように見えました。

$ git clone https://github.com/cool-RR/PySnooper
$ cd PySnooper/pysnooper/
$ find . -name '*.py' | xargs wc -l
   30 ./__init__.py
   82 ./pycompat.py
  460 ./tracer.py
   98 ./utils.py
  133 ./variables.py
  803 total

インストール

pip でインストールします。

pip install pysnooper

関数にデコレータを指定してデバッグする

デバッグしたい関数に @pysnooper.snoop() というデコレータをつけることで、該当の関数全体をデバッグ出来ます。

import pysnooper

@pysnooper.snoop()
def calc(a, b, c, d):
    tmp1 = a * b
    tmp2 = c * d
    return(tmp1 + tmp2)

calc(2, 3, 4, 5)

実行結果は以下の通りです。 実行の過程、変数への代入などが標準出力へ書き出されます。

# python sample.py
Source path:... sample.py
Starting var:.. a = 2
Starting var:.. b = 3
Starting var:.. c = 4
Starting var:.. d = 5
00:35:47.037272 call         4 def calc(a, b, c, d):
00:35:47.037773 line         5     tmp1 = a * b
New var:....... tmp1 = 6
00:35:47.037900 line         6     tmp2 = c * d
New var:....... tmp2 = 20
00:35:47.038066 line         7     return(tmp1 + tmp2)
00:35:47.038240 return       7     return(tmp1 + tmp2)
Return value:.. 26

デバッグしたい部分を with ブロックで囲む

関数全体では無く、部分的にデバッグしたい場合は該当箇所を with pysnooper.snoop(): ブロックで囲みます。

import pysnooper

def calc(a, b, c, d):
    tmp1 = a * b
    with pysnooper.snoop():
        tmp2 = c * d
    return(tmp1 + tmp2)

calc(2, 3, 4, 5)

実行結果は以下の通りです。 with ブロックで囲まれていなくても変数への代入はデバッグ表示されていました。 まだソースコードを読めていないので、この辺りの細かい挙動は掴めていません…

# python sample.py
Source path:... sample.py
New var:....... a = 2
New var:....... b = 3
New var:....... c = 4
New var:....... d = 5
New var:....... tmp1 = 6
00:39:09.728246 line         6         tmp2 = c * d

出力をファイルに保存する

PySnooper の引数にファイル名を指定するとデバッグ結果を (標準出力では無く) 指定したファイルへ書き出します。

import pysnooper

@pysnooper.snoop('debug.log')
def calc(a, b, c, d):
    tmp1 = a * b
    tmp2 = c * d
    return(tmp1 + tmp2)

calc(2, 3, 4, 5)

実際にスクリプトを実行しても、標準出力には何も表示されなくなりました。

# python sample.py
#

指定した名前のファイルが作成されており、デバッグ出力されていました。 尚、PySnooper は既存のファイル名を指定した場合、ログファイルを追記するようです。

# cat debug.log
Source path:... sample5.py
Starting var:.. a = 2
Starting var:.. b = 3
Starting var:.. c = 4
Starting var:.. d = 5
10:36:29.326940 call         4 def calc(a, b, c, d):
10:36:29.328158 line         5     tmp1 = a * b
New var:....... tmp1 = 6
10:36:29.328346 line         6     tmp2 = c * d
New var:....... tmp2 = 20
10:36:29.328615 line         7     return(tmp1 + tmp2)
10:36:29.328892 return       7     return(tmp1 + tmp2)
Return value:.. 26

デバッグ用コードを残したまま、デバッグ表示の有効/無効を切り替える

PySnooper のデバッグ用コードの有無に関わらず、デバッグ表示の有効 / 無効を切り替えるには PYSNOOPER_DISABLED 環境変数を使います。 この機能は activate / desactivate pysnooper ? #127 という Issue で議論され、実装されたもののようです。

cool-RR commented on 5 Jun 2019 @alexmojaki Great, thanks for the input!

@yvonhubert If you or anyone else would like to implement, go ahead. The environment variable name should be PYSNOOPER_DISABLED and if it has a non-empty value, then PySnooper is in no-op mode, i.e. any decorated functions will just be the original functions and the context manager will not do anything. Tests should be added.

PySnooper の tracer.py では以下のように定義されているようです。 PYSNOOPER_DISABLED 環境変数の定義があれば、DISABLED フラグを有効化するようです。 デバッグした結果を出力する際、この DISABLED フラグの論理値を見て、「出力する or しない」を判定しているようです。

DISABLED = bool(os.getenv('PYSNOOPER_DISABLED', ''))

上記コードの通り、PYSNOOPER_DISABLED 環境変数の中身は確認しておらず、「定義されているか、否か」しか確認していません。 但し、bash環境変数export する際に値が未定義だと環境変数が定義されない為、とりあえず適当に True という値を設定してみました (a でも False でも、何を定義しても同じ動作になります)。

export PYSNOOPER_DISABLED='True'

これでソースコード自体は変更せずに、デバッグ表示を無効化することが出来ました。

$ export PYSNOOPER_DISABLED='True'
$ echo $PYSNOOPER_DISABLED
True
$ python sample.py
$

尚、上述の Issus でも「有効/無効を切り替えるのはコンフィグでは無く環境変数で」という方針になったようで、実際に実装上もそのようになっています。 従って「外部のコンフィグでデバッグ表示の有効 / 無効を切り替える」ということは出来ないようです。