Masayan tech blog.

  1. ブログ記事一覧>
  2. Pythonにおける例外処理

Pythonにおける例外処理

公開日

環境

  • Python 3.9

適切な例外処理の必要性

プログラムの安定性の確保

例外が発生した場合でもプログラム全体がクラッシュすることなく安定してシステムを動作させることができる。あらかじめ発生を予測できる例外やエッジケース、プログラム上ではどうしようもない外部の要因による例外を区別し適切に捕捉処理することが重要

エラーの特定とデバッグ

エラーが発生した場所と原因を特定しやすくなる。これにより、問題の解決とデバッグが容易になる。

データの保護

例外処理を行うことで、エラーが発生した際に重要なデータが失われることを防ぐことができる。例えば、データベース操作中にエラーが発生した場合、適切な例外処理によりロールバックを行い、データの一貫性を保つことができる。

ユーザー体験の向上

全ての例外処理をきちんと捕捉して対処せず、毎回500ページをユーザーに見せてしまっていてはユーザー体験が損なわれる。例外の種類応じたエラーメッセージや解消方法をユーザーに伝えることが重要

基本的な例外

例外とは、プログラムが実行中にエラーなどの異常が発生した時に投げられる特殊なイベントである。Pythonにおいては、様々な種類の例外が定義されている。例えば数値を0で割ると、ZeroDivisionErrorという例外が、配列の存在しないインデックスにアクセスするとIndexErrorが生じる

また、raiseを使用すると意図的にエラーオブジェクトの種類を指定して例外をスローすることができる。以下は引数の型がstr型でない場合に例外をスローする例

def check_is_string(input: str):
    if not isinstance(input, str):
        raise TypeError(f"Input must be a string: {input}")
        

check_is_string(1)


# 以下実行結果
Traceback (most recent call last):
  File "main.py", line 6, in <module>
    check_is_string(1)
  File "main.py", line 3, in check_is_string
    raise TypeError(f"Input must be a string: {input}")
TypeError: Input must be a string: 1

基本的な例外処理の記述方法

Pythonにおける例外処理は、tryexceptfinallyのブロックを使用して記述される。exceptブロックの部分は例外ハンドラと呼ばれており、tryブロック内の処理を実行した際に例外が発生すると、そこで処理を中断してexceptブロックのコードを実行する

try:
    例外発生の可能性があり、その処理が必要な処理
except 例外のクラス名 as e:
    例外が発生した場合の処理
finally:
    例外が発生してもしなくても行う処理

例外が発生するとどうなるか

例外が発生すると、プログラムの実行は即座に停止し、エラーメッセージが出力される。ただし、例外が適切に捕捉されると、そのブロック内で処理が続行され、プログラムの実行が停止することはない。

例えば以下のようなケースの場合、check_is_stringで例外が発生し、プログラムがその時点で中断されるので、beginは出力されるが、endは出力されない

def check_is_string(input: str):
    if not isinstance(input, str):
        raise TypeError(f"Input must be a string: {input}")
        

print("begin")
check_is_string(1)
print("end")

例外を意図的にスローする

例外は、エラーメッセージを発生させるにも使用できる。この場合、raise文を用いて自分で例外を作成してスローすることができる。

raise ValueError('Invalid value')

発生した例外情報を取得する

例外が発生した際には、エラーの種類、エラーメッセージ、スタックトレースなどの詳細な情報を取得できる。これらの情報は、exceptブロック内でエラーオブジェクトとして取得可能である。

エラーメッセージ

try 句で 発生した 例外オブジェクト は except Exception as e: の e に格納される。また、e はBaseExceptionクラスを継承したインスタンス

dic = {"foo": 1, "bar":2}

def get_value_from_dict(dictionary, key):
    # 辞書には、渡されたkeyに対応する値が必ず存在している必要があるという前提があるとする
    val = dictionary.get(key)
    
    if val is None:
        raise Exception(f"Not found key: {key}")
    
    return val
    
   
try:
    val = get_value_from_dict(dic, "hoge")
    print(val)
except Exception as e:
    print(e)


# 以下出力内容
Not found key: hoge

発生した例外の種類

e.class.__name__から取得できる

def sample():
    raise IndexError("Occur IndexError !!")
   
try:
    sample()
except Exception as e:
    print(e.__class__.__name__) # IndexError

スタックトレース

前提知識

  • 例外が発生した際に、どのように関数が呼び出されたのか、どこでエラーが発生したのか、を特定するためのプログラムの実行過程に関する情報
  • プログラムはある処理が別の処理を呼び出し、さらにその処理が別の処理を呼び出し・・・という流れとなっているが、ある処理で例外が発生した場合に、その処理の呼び出し元に例外発生を伝えるようになっている(例外の伝播)
  • 例外が伝播しどの時点においても例外が捕捉されず、呼び出しの起点(エントリーポイント)でも処理されない場合、結果としてプログラム全体がサーバーエラーとして失敗しPythonインタプリタがstderrにスタックトレースを出力する

取得方法

  • exceptで例外を捕捉している場合は以下のように、tracebackのformat_excメソッドにより、スタックトレースを取得できる
  • スタックトレースには、どのファイルの何行目のどの関数でどのような例外が発生しているのかという詳細な情報が含まれている
import traceback

def check_is_string(input: str):
    if not isinstance(input, str):
        raise TypeError(f"Input must be a string: {input}")
        
try:
    print("begin")
    check_is_string(1)
    print("end")
except Exception as e:
    print(traceback.format_exc())

# 以下出力内容
begin
Traceback (most recent call last):
  File "main.py", line 9, in <module>
    check_is_string(1)
  File "main.py", line 5, in check_is_string
    raise TypeError(f"Input must be a string: {input}")
TypeError: Input must be a string: 1

例外処理と自動テスト

例外処理をプロダクトコードに記述するだけでは不十分。自動テストで想定する例外が発生することをアサートするテストケースをしっかり用意しておくことが大切

例外を捕捉する際のポイント

原則キャッチしない

Fail fastの考えから、原則として例外はcatchせず、そのままプログラムを落とすことが望ましい。プログラムが落ちるとユーザーには500エラーページが表示され、開発者にスタックトレースを含むエラー通知が届き、開発者がエラーを調査してプログラムを修正するという流れを構築することができる

変にキャッチして落ちるはずの欠陥のあるプログラムがそのまま動いてしまうと、被害が大きくなる可能性が高いのと、本来例外が発生するべき箇所以外で例外が発生すると、原因の特定が困難になる可能性が高い。

また、大抵の言語には開発者のプログラムミスなどが起因で発生する例外(SyntaxErrorやNameErrorなど)があるが、これをcatchしている場合はその時点で使い方がおかしいので、すぐにそのソースコードを修正するべきである(catchしてのプログラマのミスを握りつぶさない)

キャッチする場合

catchするとしてもユーザーに適切なエラーメッセージを返却し、スタックトレース等エラーの詳細ががきちんとslackなり開発者に通知できるようにすべき。なぜなら、catchして何もハンドリングを行わないということが行われると、例外が発生したことを、ユーザー・プログラマ含め、誰も知ることができないので不備のあるプログラムがそのまま実行され続けるという一番恐ろしい現象が生じてしまうためである。

また、catchしてprintで予期せぬエラーが発生しました。と表示するだけやエラーオブジェクトを出力するだけのコードはデバッグ時にスタックトレースが確認できないのと、能動的にサーバーログを見に行かないとエラーを確認できないようでは初動が遅れるので必ずエラーの詳細を開発者が確認できる状態にしておくべき

想定できる例外

開発者が想定できなかった例外発生に対する対応などは上記の原則に従いキャッチしないという考え方で問題ないが、あらかじめ想定できるものについてはわざわざプログラム全体を落としてユーザー体験を低下させる必要はない。

ユーザーに500エラーページを表示させるのではなく、ページはそのままでトースターなどを表示し、ユーザーに対してどのようなエラーが起きており、何をすることで解消できるのか、何をすれば良いのかということを伝えるのがベター。毎回500エラーページへ飛ばされていては、ストレスが溜まるので想定できる例外についてはcatchしてエラーメッセージを画面に表示する等がよい

try:
    sample()
except Exception as e:
    # catchし、開発者に通知を送りつつ、エラーメッセージを含むjsonレスポンスなりを返却する

付加情報を追加したい時

実際に例外発生した際の一意の識別子等をログに追加することでデバッグがしやすくなるので、そのために一度catchするというケースは結構ある

def process_payment(payment_id):
    try:
        # 決済処理を行うコードをここに書く
        pass
    except Exception as e:
        # 決済が失敗した場合、エラーメッセージとともにpayment_idをログに出力、slackに通知などする
        logging.error(f'Payment failed for payment_id: {payment_id}, due to error: {e}')
        
        # エラーメッセージを含むjsonレスポンスを返す

バッチなど

大量のデータを扱うバッチ処理などでは一部の不正なデータが存在することにより例外発生する可能性も考えられるが、このような処理では往々にしてバッチ全体を終了させてしまうよりも一部の不正なデータに対する処理時に発生した例外はキャッチしてスキップし、それ以外の処理を継続するということが許容されるケースが多い。

ただ、この場合もバッチ処理中にどのデータが失敗したのかを特定する情報や、全体の何件の内何件が失敗したのか、どのような例外が発生したのか等の情報をきっちりと記録し開発者に通知できるようにしておく必要がある。

キャッチして投げ返す

例外をキャッチして再度例外をスローするというケースがある。例えば、別のより具体的な例外クラス(独自に定義した例外クラス)を呼び出しもとに投げ返したい時など

複数の例外を捕捉する

例外を捕捉する際には、対象とする例外の種類をできるだけ具体的に指定することが推奨される。すべての例外を捕捉すると、意図しない例外も捕捉してしまい、デバッグが困難になる可能性があるためである。

以下のように、exceptを複数記述すれば、複数の例外オブジェクトを捕捉すること可能。

try:
   ・・・
except TypeError as e:
   ・・・
except NameError as e:
   ・・・

また、Exceptionクラスは全ての例外クラスの親クラスとなっているので、以下のようにExceptionとして広く受けることで、全ての例外を補足することが可能

try:
    ・・・
except Exception as e:
    ・・・

ただし、全てをExceptionで広く受けるとコードの意図が伝わりにくいのでこの処理ではどのような例外が発生しうるのかということを込める意味合いでも、以下のように想定できる例外を先に記述し、最後にExceptionとして例外を受けるようにしておいた方がいい。この場合、TypeError以外の例外が発生した場合はexcept Exception・・・内の処理が実行されることになる

def sample():
    raise TypeError("Occur TypeError !!")
   
try:
    sample()
except TypeError as e:
    print(e)
    
except Exception as e:
    print("予期せぬ例外が発生しました")

まとめ

いかがでしたでしょうか。本記事では、Pythonの例外処理ついて紹介しています。例外の基礎や種類、アンチパターンなどを実務のユースケースを交えながら具体的に説明していますので、ぜひ参考にしてみてください。