Python デバッグ技術

PythonBeginner
オンラインで実践に進む

はじめに

コンピュータプログラミングにおいて、デバッグとは、プログラムが正しく動作するのを妨げる欠陥や「バグ」を見つけ出し、修正するための不可欠なプロセスです。バグはソフトウェア開発において避けられないものですが、Python はそれらを効率的に診断し修正するのに役立つ強力なツールセットを提供しています。

この実験(Lab)では、Python におけるいくつかの基本的なデバッグ技術を学びます。具体的には、以下の方法について解説します。

  • エラー発生時にプログラムの流れを制御するために、例外を手動で発生させ、処理する方法。
  • ロギング目的のために、詳細なエラートレースバックを文字列としてキャプチャする方法。
  • 論理エラーを早期に検出するために、アサーション(assertion)を健全性チェックとして実装する方法。
  • プログラムの実行を中断することなく、その実行を追跡し、問題を診断するための柔軟なロギングシステムを実装する方法。

例外の発生 (Raising Exceptions)

このステップでは、raise ステートメントを使用して例外を手動で発生させる方法を学びます。例外は、エラーが発生したことをシグナルを送るための手段です。raise ステートメントは、raise キーワード、Exception() 関数の呼び出し、および Exception() に渡される役立つエラーメッセージ文字列で構成されます。

関数の前提条件が満たされていない場合にその関数内で例外を発生させ、その後、その関数を呼び出すコード側で try ブロックと except ブロックを使用してこれらの例外を処理するのが一般的な慣行です。

注記: WebIDE (VS Code のようなもの) の使用に慣れていない場合は、まず こちらの実験 (lab) で学習してください。

例を見てみましょう。

  1. 左側のファイルエクスプローラーからファイル raise_exception.py を開きます。
  2. ファイルに次のコードを追加します。このコードは、無効な引数に対して例外を発生させる関数 box_print を定義し、その後、有効な入力と無効な入力の両方でこの関数を呼び出し、例外を捕捉します。
def box_print(symbol, width, height):
    if len(symbol) != 1:
      raise Exception('Symbol must be a single character string.')
    if width <= 2:
      raise Exception('Width must be greater than 2.')
    if height <= 2:
      raise Exception('Height must be greater than 2.')
    print(symbol * width)
    for i in range(height - 2):
        print(symbol + (' ' * (width - 2)) + symbol)
    print(symbol * width)

## Handle exceptions when calling the function
for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
    try:
        box_print(sym, w, h)
    except Exception as err:  ## Catch exception and print error message
        print('An exception happened: ' + str(err))
  1. 次に、ターミナルからスクリプトを実行し、例外がどのように発生し、処理されるかを確認します。
python3 raise_exception.py

以下の出力が表示されます。box_print への最初の 2 回の呼び出しは成功しますが、最後の 2 回は失敗し、except ブロックがトリガーされてカスタムエラーメッセージが出力されます。

****
*  *
*  *
****
OOOOOOOOOOOOOOOOOOOO
O                  O
O                  O
O                  O
OOOOOOOOOOOOOOOOOOOO
An exception happened: Width must be greater than 2.
An exception happened: Symbol must be a single character string.

トレースバックを文字列として取得する

未処理の例外が発生すると、Python はトレースバック(エラーに至った関数呼び出しの詳細なレポート)を出力します。場合によっては、例外を優雅に処理しつつも、後でデバッグするために完全なトレースバックを記録しておきたいことがあります。traceback モジュールを使用すると、これが可能です。

このステップでは、traceback.format_exc() を使用してトレースバックを文字列としてキャプチャし、ファイルに保存する方法を学びます。

  1. ファイルエクスプローラーからファイル get_traceback.py を開きます。
  2. 次のコードを追加します。このスクリプトは traceback モジュールをインポートし、try...except ブロックを使用して例外を捕捉します。except ブロック内で、完全なトレースバックを errorInfo.txt という名前のファイルに書き込みます。
import traceback

try:
    raise Exception('This is the error message.')
except:
    with open('errorInfo.txt', 'w') as error_file:
        error_file.write(traceback.format_exc())  ## Write traceback to file
    print('The traceback info was written to errorInfo.txt.')
  1. ターミナルでスクリプトを実行します。
python3 get_traceback.py

スクリプトは確認メッセージを出力します。

The traceback info was written to errorInfo.txt.
  1. 次に、新しく作成されたファイル errorInfo.txt の内容を確認します。ファイルエクスプローラーから開くか、ターミナルで cat コマンドを使用できます。
cat errorInfo.txt

ファイルには、以下のような完全なトレースバックが含まれます。

Traceback (most recent call last):
  File "/home/labex/project/get_traceback.py", line 4, in <module>
    raise Exception('This is the error message.')
Exception: This is the error message.

このテクニックは、プログラムをクラッシュさせることなく、本番アプリケーションでエラーをロギングする際に非常に役立ちます。

アサーションの使用

アサーションは、コードが明らかに間違ったことをしていないことを確認するための健全性チェックです。assert ステートメントは、ある条件が真であるかどうかをチェックします。条件が偽の場合、AssertionError が発生し、理想的にはプログラムがクラッシュするはずです。この「フェイルファスト (fail-fast)」のアプローチは、バグの根本原因をより迅速に特定するのに役立ちます。

assert ステートメントは、assert キーワード、チェックする条件、およびオプションのメッセージで構成されます。

このステップでは、アサーションを使用してプログラムの状態を検証します。

  1. ファイルエクスプローラーからファイル use_assertions.py を開きます。
  2. 次のコードを追加します。これは pod_bay_door_status の状態をチェックします。最初のアサーションは何も出力せずに通過しますが、2 番目のアサーションは失敗し、AssertionError を発生させます。
## assert statement: check condition, raise AssertionError if False
pod_bay_door_status = 'open'
assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'  ## Passes

pod_bay_door_status = 'I\'m sorry, Dave. I\'m afraid I can\'t do that.'
assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'  ## Raises AssertionError
  1. ターミナルからスクリプトを実行します。
python3 use_assertions.py

2 番目のアサーションが失敗するため、プログラムはクラッシュし、カスタムメッセージ付きの AssertionError が表示されます。

Traceback (most recent call last):
  File "/home/labex/project/use_assertions.py", line 6, in <module>
    assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'
AssertionError: The pod bay doors need to be "open".

例外とは異なり、アサーションを try...except で処理すべきではありません。AssertionError は修正が必要なプログラム内のバグを示しています。また、アサーションは Python を -O (最適化) フラグ付きで実行することでグローバルに無効にできるため、常に強制されるべき入力検証には使用すべきではない点にも注意が必要です。

ロギングの実装

デバッグに print() ステートメントを使用することもできますが、より強力で柔軟なアプローチは、Python の組み込みの logging モジュールを使用することです。ロギングを使用すると、プログラムの実行中にイベントを追跡し、メッセージを重大度(severity)で分類できます。

このステップでは、バグの診断に役立てるために、プログラムにロギングを追加します。

  1. まず、ファイルエクスプローラーからファイル factorial_log.py を開きます。
  2. 次のコードを追加します。このプログラムは数値の階乗を計算しようとしますが、バグが含まれています。各ステップで変数の値をトレースするために logging.debug() 呼び出しを追加しました。logging.basicConfig() 関数は、ロギングシステムを設定し、DEBUG レベル以上のメッセージを表示するようにします。
import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
logging.debug('Start of program')

def factorial(n):
    logging.debug('Start of factorial(%s)' % (n))
    total = 1
    for i in range(1, n + 1):
        total *= i
        logging.debug('i is ' + str(i) + ', total is ' + str(total))
    logging.debug('End of factorial(%s)' % (n))
    return total

print(factorial(5))
logging.debug('End of program')
  1. ターミナルからスクリプトを実行します。
python3 factorial_log.py

プログラムの通常の出力に混じって、詳細なログ出力が表示されます。

 %(asctime)s - DEBUG - Start of program
 %(asctime)s - DEBUG - Start of factorial(5)
 %(asctime)s - DEBUG - i is 1, total is 1
 %(asctime)s - DEBUG - i is 2, total is 2
 %(asctime)s - DEBUG - i is 3, total is 6
 %(asctime)s - DEBUG - i is 4, total is 24
 %(asctime)s - DEBUG - i is 5, total is 120
 %(asctime)s - DEBUG - End of factorial(5)
120
 %(asctime)s - DEBUG - End of program

ログメッセージを調べることで、ループの各イテレーションにおける i と total の値を明確に確認でき、プログラムの流れを理解しバグを見つけるための強力な方法となります。

ロギング動作の設定

logging モジュールは高度に設定可能です。表示するメッセージの重大度レベルを制御したり、ロギングを完全に無効にしたり、ログメッセージを画面ではなくファイルにリダイレクトしたりできます。

ロギングレベル (Logging Levels)

5 つの標準的なロギングレベルがあり、それぞれに対応する関数があります。

レベル (Level) ロギング関数 (Logging Function) 説明 (Description)
DEBUG logging.debug() 小さな詳細情報用。問題診断時のみ有用。
INFO logging.info() 一般的なイベントの記録、または物事が期待通りに動作していることの確認用。
WARNING logging.warning() プログラムの実行を停止させない潜在的な問題を示すため。
ERROR logging.error() 特定の操作が失敗した原因となったエラーを記録するため。
CRITICAL logging.critical() プログラムの終了を引き起こしそうな致命的なエラー用。

basicConfig() でロギングレベルを設定すると、Python はそのレベル以上のすべてのメッセージを表示します。例えば、level=logging.INFO を設定すると、INFO、WARNING、ERROR、CRITICAL メッセージは表示されますが、DEBUG は表示されません。

ロギングの無効化 (Disabling Logging)

デバッグが完了した後、コードから logging 呼び出しを削除せずに、すべてのログメッセージをオフにしたい場合があります。これは logging.disable() を使用して実行できます。引数としてレベルを受け取り、そのレベル以下のすべてのログメッセージを抑制します。例えば、logging.disable(logging.CRITICAL) はすべてのログメッセージを抑制します。

ファイルへのロギング (Logging to a File)

ログをコンソールに出力する代わりに、logging.basicConfig() に filename キーワード引数を渡すことでファイルに保存できます。

実際に試してみましょう。

  1. ファイル configure_logging.py を開きます。
  2. 次のコードを追加します。これは、DEBUG レベル以上のすべてのメッセージを myProgramLog.txt という名前のファイルに書き込むようにロギングを設定します。
import logging

## Configure logging to write to a file
logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

logging.debug('This is a debug message.')
logging.info('This is an info message.')
logging.warning('This is a warning message.')
  1. ターミナルからスクリプトを実行します。
python3 configure_logging.py

今回は、ターミナルには何も出力されません。

  1. ファイル myProgramLog.txt の内容を確認します。
cat myProgramLog.txt

タイムスタンプと重大度レベルがすべて含まれたログメッセージがファイルに書き込まれていることがわかります。

2023-10-27 10:30:00,123 - DEBUG - This is a debug message.
2023-10-27 10:30:00,123 - INFO - This is an info message.
2023-10-27 10:30:00,123 - WARNING - This is a warning message.

これで、基本的な Python デバッグ技術の解説は終了です。

まとめ

この実験(Lab)では、いくつかの重要な Python デバッグ技術を学習し、実践しました。まず、raise ステートメントを使用してカスタム例外を作成および発生させ、try...except ブロックでそれらを処理する方法から始めました。次に、ロギング目的で完全な例外トレースバックを文字列としてキャプチャするために traceback.format_exc() を使用する方法を学びました。

続いて、assert ステートメントを探求し、プログラムが予期しない状態に陥った場合に健全性チェック(sanity checks)を実行し、「早期に失敗 (fail fast)」させる方法を学びました。最後に、logging モジュールに踏み込み、メッセージを表示するように設定する方法、異なるロギングレベルで出力を制御する方法、および永続的な記録のためにログをファイルに書き込む方法を学習しました。

これらのスキルは、堅牢で保守性が高く、デバッグしやすい Python アプリケーションを作成するための基礎となります。