Pythonデバッグの究極のガイド


注:これは、もともとは11で投稿されましたmartinheinz.dev
たとえあなたが明確で読みやすいコードを書いたとしても、たとえあなたが非常に経験を積んだ開発者であっても、あなたのコードをテストでカバーしても、奇妙なバグは必然的に現れ、何らかの方法でそれらをデバッグする必要があります.多くの人々のリゾートだけの束を使用してprint ステートメントは、コードで何が起こっているかを確認します.このアプローチは理想的なものではなく、あなたのコードで何が間違っているのかを知るには、より良い方法があります.

ログは必須です
いくつかの並べ替えのセットアップなしでアプリケーションを書く場合は、最終的にそれを後悔するようになります.あなたのアプリケーションから任意のログを持っていないことは非常に困難なバグをトラブルシューティングすることができます.幸運にも- Pythonでは、基本的なロガーの設定はとても簡単です.
import logging
logging.basicConfig(
    filename='application.log',
    level=logging.WARNING,
    format= '[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s',
    datefmt='%H:%M:%S'
)

logging.error("Some serious error occurred.")
logging.warning('Function you are using is deprecated.')
これは、ログファイルの書き込みを開始するために必要なファイルです.このファイルは、このようになりますlogging.getLoggerClass().root.handlers[0].baseFilename ):
[12:52:35] {<stdin>:1} ERROR - Some serious error occurred.
[12:52:35] {<stdin>:1} WARNING - Function you are using is deprecated.
このセットアップは、それが十分に良いように見えるかもしれません(そして、しばしば)、しかし、よく構成されて、フォーマットされて、読み込み可能なログを持つことはあなたの人生をとても簡単にすることができます.設定を改良し拡張する一つの方法は.ini or .yaml ロガーによって読み取られるファイル.設定で何ができるかの例として:
version: 1
disable_existing_loggers: true

formatters:
  standard:
    format: "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s"
    datefmt: '%H:%M:%S'

handlers:
  console:  # handler which will log into stdout
    class: logging.StreamHandler
    level: DEBUG
    formatter: standard  # Use formatter defined above
    stream: ext://sys.stdout
  file:  # handler which will log into file
    class: logging.handlers.RotatingFileHandler
    level: WARNING
    formatter: standard  # Use formatter defined above
    filename: /tmp/warnings.log
    maxBytes: 10485760 # 10MB
    backupCount: 10
    encoding: utf8

root:  # Loggers are organized in hierarchy - this is the root logger config
  level: ERROR
  handlers: [console, file]  # Attaches both handler defined above

loggers:  # Defines descendants of root logger
  mymodule:  # Logger for "mymodule"
    level: INFO
    handlers: [file]  # Will only use "file" handler defined above
    propagate: no  # Will not propagate logs to "root" logger
Pythonコードの中でこのような豊富な設定をすることは、ナビゲートしたり編集したりするのが難しいでしょう.YAMLファイルで物事を維持することははるかに簡単にセットアップし、上記のような非常に特定の設定で複数のロガーを微調整します.
これらの設定フィールドがどこから来たのか疑問に思っているなら、これらは文書化されますhere それらのほとんどは最初の例に示すようなキーワード引数です.
それで、現在ファイルで設定を持っていることは、ロードする必要があることを意味します.YAMLファイルで最も簡単な方法
import yaml
from logging import config

with open("config.yaml", 'rt') as f:
    config_data = yaml.safe_load(f.read())
    config.dictConfig(config_data)
Pythonのロガーは実際に直接YAMLファイルをサポートしていませんが、それはYAMLを使用して簡単に作成できる辞書の設定をサポートしていますyaml.safe_load . あなたが古い使用する傾向があるならば.ini ファイルは、私はちょうど辞書の設定を使用してdocs . より多くの例logging cookbook .

ログ作成者
前のログのヒントを続けて、いくつかのバギー関数のログコールが必要な状況になる可能性があります.特定のログレベルとオプションのメッセージですべての関数呼び出しを記録するログデコレータを使用することができます.デコレータを見ましょう
from functools import wraps, partial
import logging

def attach_wrapper(obj, func=None):  # Helper function that attaches function as attribute of an object
    if func is None:
        return partial(attach_wrapper, obj)
    setattr(obj, func.__name__, func)
    return func

def log(level, message):  # Actual decorator
    def decorate(func):
        logger = logging.getLogger(func.__module__)  # Setup logger
        formatter = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        handler = logging.StreamHandler()
        handler.setFormatter(formatter)
        logger.addHandler(handler)
        log_message = f"{func.__name__} - {message}"

        @wraps(func)
        def wrapper(*args, **kwargs):  # Logs the message and before executing the decorated function
            logger.log(level, log_message)
            return func(*args, **kwargs)

        @attach_wrapper(wrapper)  # Attaches "set_level" to "wrapper" as attribute
        def set_level(new_level):  # Function that allows us to set log level
            nonlocal level
            level = new_level

        @attach_wrapper(wrapper)  # Attaches "set_message" to "wrapper" as attribute
        def set_message(new_message):  # Function that allows us to set message
            nonlocal log_message
            log_message = f"{func.__name__} - {new_message}"

        return wrapper
    return decorate

# Example Usage
@log(logging.WARN, "example-param")
def somefunc(args):
    return args

somefunc("some args")

somefunc.set_level(logging.CRITICAL)  # Change log level by accessing internal decorator function
somefunc.set_message("new-message")  # Change log message by accessing internal decorator function
somefunc("some args")
うそをつくつもりはない、これはあなたの頭を包むために少しかかるかもしれない(あなたはそれを貼り付けて、それを使用してコピーすることがあります).ここにある考えはlog 関数は引数をとり、内部で利用可能になりますwrapper 関数.これらの引数は、次に、デコレータに接続されているアクセッサ関数を追加することによって調整可能です.に関してはfunctools.wraps デコレータ-私たちがここでそれを使用しなかったなら、機能の名前func.__name__ ) デコレータの名前で上書きされます.しかし、それは問題です、我々が名前を印刷したいので.これはfunctools.wraps 関数名、docstringおよび引数リストをデコレータ関数にコピーします.
とにかく、これは上のコードの出力です.きれいなきちんとした?
2020-05-01 14:42:10,289 - __main__ - WARNING - somefunc - example-param
2020-05-01 14:42:10,289 - __main__ - CRITICAL - somefunc - new-message
__repr__ より読み込み可能なログ
よりデバッグ可能にするコードへの容易な改善は__repr__ クラスへのメソッド.このメソッドに慣れていない場合は、クラスのインスタンスの文字列表現を返します.ベストプラクティス__repr__ メソッドはインスタンスを再現するために使用できるテキストを出力することです.例えば、
class Circle:
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius

    def __repr__(self):
        return f"Rectangle({self.x}, {self.y}, {self.radius})"

...
c = Circle(100, 80, 30)
repr(c)
# Circle(100, 80, 30)
上記のように表現するオブジェクトが望ましくないか、可能でないなら、良い代替は使用を使用することです<...> , 例えば<_io.TextIOWrapper name='somefile.txt' mode='w' encoding='UTF-8'> .
離れて__repr__ , また、実装する良いアイデアだ__str__ 時に使用するメソッドprint(instance) が呼ばれる.これらの2つの方法で、あなたの変数を印刷するだけで、たくさんの情報を得ることができます.
__missing__ 辞書のdunderメソッド
何らかの理由でカスタム辞書クラスを実装する必要がある場合はKeyError あなたが実際に存在しないいくつかのキーにアクセスしようとするときs.コードの中でpokeすることを避けるために、どのキーが不足しているかを参照してください__missing__ 毎回呼び出されるメソッドKeyError が送出されます.
class MyDict(dict):
    def __missing__(self, key):
        message = f'{key} not present in the dictionary!'
        logging.warning(message)
        return message  # Or raise some error instead
上記の実装は非常に単純であり、不足しているキーでメッセージを返して、ログメッセージだけです、しかし、あなたはコードで間違って行ったことに関してあなたにより多くの文脈を与えるために他の貴重な情報を記録することもできました.

アプリケーションのクラッシュデバッグ
あなたのアプリケーションがクラッシュする前に何が起こっているかを確認するチャンスを得る場合は、このトリックは非常に有用かもしれない.
アプリケーションの実行-i 引数python3 -i app.py ) プログラムが終了するとすぐに、それは対話的なシェルを開始します.その時点で変数と関数を検査できます.
それが十分でないならば、あなたはより大きなハンマーを持ってくることができますpdb - Pythonデバッガ.pdb 独自の記事を正当化するいくつかの機能があります.しかし、ここでは、最も重要なビットの例とランダウンです.最初の小さなクラッシュスクリプトを見てみましょう.
# crashing_app.py
SOME_VAR = 42

class SomeError(Exception):
    pass

def func():
    raise SomeError("Something went wrong...")

func()
今、我々はそれを実行する場合-i 引数をデバッグする機会があります.
# Run crashing application
~ $ python3 -i crashing_app.py
Traceback (most recent call last):
  File "crashing_app.py", line 9, in <module>
    func()
  File "crashing_app.py", line 7, in func
    raise SomeError("Something went wrong...")
__main__.SomeError: Something went wrong...
>>> # We are interactive shell
>>> import pdb
>>> pdb.pm()  # start Post-Mortem debugger
> .../crashing_app.py(7)func()
-> raise SomeError("Something went wrong...")
(Pdb) # Now we are in debugger and can poke around and run some commands:
(Pdb) p SOME_VAR  # Print value of variable
42
(Pdb) l  # List surrounding code we are working with
  2     
  3     class SomeError(Exception):
  4         pass
  5     
  6     def func():
  7  ->     raise SomeError("Something went wrong...")
  8     
  9     func()
[EOF]
(Pdb)  # Continue debugging... set breakpoints, step through the code, etc.
上記のデバッグセッションは、あなたが何をすることができるかを非常に簡潔に示しますpdb . プログラム終了後、インタラクティブデバッグセッションに入ります.まず、インポートpdb とデバッガを起動します.その時点で我々はすべてを使用することができますpdb コマンド.上記の例として、p コマンドとリストコードを使用l コマンド.ほとんどの場合、おそらくブレークポイントを設定したいb LINE_NO ブレークポイントがヒットするまでプログラムを実行します(c ) を実行し、s , オプションで印刷可能なStackTracew . コマンドの完全なリストについては pdb docs .

トレーストレースの検査
例えば、あなたのコードが対話的なデバッグセッションを得ることができないリモートサーバーで実行しているフラスコまたはdjangoアプリケーションであると言いましょう.その場合にはtraceback and sys あなたのコードで失敗していることについてのより多くの洞察を得るパッケージ
import traceback
import sys

def func():
    try:
        raise SomeError("Something went wrong...")
    except:
        traceback.print_exc(file=sys.stderr)
Runの場合、上記のコードが送出された最後の例外を出力します.例外を印刷することは別として、使用することもできますtraceback StackTraceを印刷するパッケージtraceback.print_stack() ) または生のスタックフレームを取り出し、それをフォーマットし、さらに検査します(traceback.format_list(traceback.extract_stack()) ).

デバッグ中のモジュールの再読み込み
場合によっては、デバッグや実験的なシェルでいくつかの機能を試して、頻繁に変更を行うことがあります.実行/テストのサイクルを簡単に変更するには、実行することができますimportlib.reload(module) すべての変更後に対話セッションを再起動する必要はありません.
>>> import func from module
>>> func()
"This is result..."

# Make some changes to "func"
>>> func()
"This is result..."  # Outdated result
>>> from importlib import reload; reload(module)  # Reload "module" after changes made to "func"
>>> func()
"New result..."
このチップは、デバッグより効率的です.それは常にいくつかの不必要な手順をスキップして、ワークフローをより速く、より効率的にすることができるように素晴らしいです.一般に、モジュールを再読み込みするのは良いアイデアです.

Debugging is an Art.



結論
ほとんどの時間、実際に何のプログラミングは-試行錯誤の多くです.一方、私の意見では-私の意見では-芸術とそれに良い時間と経験がかかります-あなたが使用するライブラリやフレームワークを知っているほど、簡単に取得します.上記のヒントとトリックは、デバッグを少し効率的で高速にすることができますが、これらのPython特有のツールを別にすれば、デバッグへの一般的なアプローチを理解したいかもしれませんThe Art of Debugging レミーシャープによって.