Python の例外を別の例外として投げるときの話

記事タイトルとURLをコピーする

はじめに

こんにちは。技術4課の保田(ほだ)です。

iOS14 へのアップデートをしたらかなり雰囲気が変わってビックリしています。

というわけで(?)今日は Python のお話です。

要約

raise 文には from が使えるぞ

本題

main 関数から呼び出されるある関数があったとして、次のような例外処理をしたいとします。

特定の例外クラス以外は全部一つの例外クラスでまとめた上で、改めて main 関数でキャッチしたい!

ややこしいので例を挙げます。

import traceback
 
def main():
    try:
        target()
        print('success')
    except Exception:
        print(traceback.format_exc())    
 
 
def target():
    try:
        1/0  # ZeroDivisionError が起きる
    except IndexError:
        raise
    except Exception as err:
        raise Exception("IndexError 以外の何かしらのエラーが発生しました")  
 
 
if __name__ == '__main__':
    main()

main 関数から target 関数を呼び出しています。 どちらの関数も try-except 構文を使い、 target 関数の方は発生した例外は main 関数に投げるようになっています。

この target 関数は 1/0 (つまりゼロ除算)があるので明らかに ZeroDivisionError しか発生しないことが分かります。

が、あくまでサンプルなので、もう少しいろいろな例外が発生しうる処理があると想像してください。

その中で、 IndexError が発生したときだけそのまま IndexError として main 関数に投げるようになっています。

まず、この 1/0[][0] に変えて実行してみます。

$ python sample.py 
Traceback (most recent call last):
  File "sample.py", line 5, in main
    target()
  File "sample.py", line 15, in target
    [][0]  # IndexError
IndexError: list index out of range

IndexError として出力されていますね。 main 関数の except 節内の print(traceback.format_exc()) によってスタックトレースが文字列形式で出力されます。

では [][0]1/0 に戻してまた実行してみます。

$ python sample.py 
Traceback (most recent call last):
  File "sample.py", line 14, in target
    1/0  # ZeroDivisionError
ZeroDivisionError: division by zero
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "sample.py", line 5, in main
    target()
  File "sample.py", line 18, in target
    raise Exception("IndexError 以外の何かしらのエラーが発生しました")
Exception: 何かしらのエラーが発生しました

ちょっとイカつい感じになりました。 特に気になるのがこの一文です。

During handling of the above exception, another exception occurred:

日本語に訳すと「上記の例外(ゼロ除算)の処理中に別の例外が発生しました」となります。

なんだか意図しないエラーが起きてしまったような気がして心穏やかではないですね。

もうちょっとイイ感じにしてみましょう。

といっても実は 例外クラスのドキュメント を見ると冒頭にガッツリ書いてあります。

現在処理中の例外を raise を使って再送出するのではなく新規に例外を送出する場合、raise と一緒に from を使うことで暗黙の例外コンテキストを捕捉することができます

これだけ読むと何言ってるのかようわからんという感じですので、さっそく試してみましょう。

def target():
    try:
        1/0  # ZeroDivisionError が起きる
    except IndexError:
        raise
    except Exception as err:
        raise Exception("IndexError 以外の何かしらのエラーが発生しました") from err

raise 文の末尾に from err を追加しました。

$ python sample.py 
Traceback (most recent call last):
  File "sample.py", line 14, in target
    1/0  # ZeroDivisionError
ZeroDivisionError: division by zero
 
The above exception was the direct cause of the following exception:
 
Traceback (most recent call last):
  File "sample.py", line 5, in main
    target()
  File "sample.py", line 20, in target
    raise Exception("IndexError 以外の何かしらのエラーが発生しました") from err
Exception: IndexError 以外の何かしらのエラーが発生しました

先ほど気になっていた一文がちょっとだけ変わりました。

The above exception was the direct cause of the following exception:

「上記の例外(ゼロ除算)は、次の例外の直接の原因です」といった感じでしょうか。

これだと最終的に出力されるエラーメッセージ「何かしらのエラーが発生しました」(私が勝手に決めたものですが)の直接の原因はゼロ除算なので、対応すべきは ZeroDivisionError だな、というのが分かりやすいので良いですね。

また、 先ほどのドキュメント の続きには次の記述があります。

from に続く式は例外か None でなくてはなりません。

None も使えるようです。試してみましょう。

def target():
    try:
        1/0  # ZeroDivisionError が起きる
    except IndexError:
        raise
    except Exception as err:
        raise Exception("IndexError 以外の何かしらのエラーが発生しました") from None

実行してみるとこうなります。

$ python sample.py 
Traceback (most recent call last):
  File "sample.py", line 5, in main
    target()
  File "sample.py", line 19, in target
    raise Exception("IndexError 以外の何かしらのエラーが発生しました") from None
Exception: IndexError 以外の何かしらのエラーが発生しました

実際のエラーの詳細が Exception: 何かしらのエラーが発生しました に丸め込まれています。

何かしらのライブラリを作る際に、細かいエラーを丸め込んで一つのユーザー定義のエラーとして呼び出し元に返したいときはこちらの方が良いかもしれません。

まとめ

エラーメッセージをイイ感じにしたい際は参考にしていただければと思います。

参考

保田 和馬 (記事一覧)

アプリケーションサービス部

ボールペンで字を書くことがあります。(書かないこともある)