🔍

実は知らないPythonのraise

に公開

Pythonのraiseといえば当然書いててよく出てくるんですが・・・
これに限らず基本的な文法って基本的すぎて実はドキュメントの詳細を読んだことがない人が多かったりして、自分も含めて詳細を知らないことが多いんですよね。

なんで今回はふと書いてて疑問に思ったraiseの仕様の詳細を紹介していこうと思います。

raise

Pythonでraiseするとき、通常はおそらくこんな感じで書いてるはずですが・・・

raise ValueError()

実はこうかけます。

raise ValueError

raiseはインスタンスじゃなくていい

そうです。実はPythonのドキュメントではクラスかインスタンスを指定する必要があると書かれています。
なので引数が必要ない場合は実はインスタンス化する必要はありません。

これは例外インスタンスか例外クラス(BaseException を継承したクラス、たとえば Exception やそのサブクラス)でなければなりません。 例外クラスが渡された場合は、引数無しのコンストラクタが呼び出され、暗黙的にインスタンス化されます

https://docs.python.org/ja/3.12/tutorial/errors.html#raising-exceptions

周りにも聞いてみたんですが、かなりPythonを長く書いてるエンジニアも以外と知っている人が少ないです。

raise ... from ...

実はraiseでもfrom構文が使えます。
具体的にいつ使うのかというと、例外処理中に別の例外を送出する場合です。

def process_data(data):
    try:
        result = int(data)
    except ValueError as e:
        raise TypeError("int型に変換することは出来ません") from e

try:
    process_data("abc")
except TypeError as e:
    print(f"例外が発生しました: {e}")
    print(f"元の例外: {e.__cause__}")

https://docs.python.org/ja/3.12/tutorial/errors.html#exception-chaining

あまりこういったケースでfromをきっちり使っていることはないですが、本来はraise from構文を使用するのが推奨されます。

通常のraiseとの違い

結論から言うと、エラー時のメッセージが異なります。
はい、おそらくそれだけです・・・。

ただあえてメリットを挙げるなら、このメッセージの違いでその例外が意図的に送出されたものなのかが判断しやすくなります。

ValueError: invalid literal for int() with base 10: 'test'

During handling of the above exception, another exception occurred:
ValueError: invalid literal for int() with base 10: 'test'

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

詳細

さてこの挙動の違いですが、全ての例外の基底クラスであるBaseExceptionクラスにはいくつかの属性がありますが、例外取り扱い時には主に__cause__, __context__, __suppress_context__の3つが参照されます。

このうち__cause__はその例外の直接の原因である例外が、__context__にはその例外が発生するまでの元の例外が保存されます。
そしてraise from構文を使用すると__cause__に元の例外が保存された上で__suppress_context__Treuに設定されます。

そして__context__が参照されるのは__cause__がNoneかつ__suppress_context__Falseの場合なので、raise fromを使うと__cause__の情報が表示されることになります。

def convert(s: str):
    try:
        return int(s)
    except ValueError as e:
        raise TypeError from e

try:
    convert("test")
except TypeError as e:
    print(e.__cause__)
    print(e.__context__)
    print(e.__suppress_context__)

# raise fromの場合
# invalid literal for int() with base 10: 'test'
# invalid literal for int() with base 10: 'test'
# True

# 通常のraiseの場合
# None
# invalid literal for int() with base 10: 'test'
# False

raise from None

ドキュメントにもありますが、特殊な使い方としてraise ... from Noneとすることで完全に新しい例外として置き換え、元の例外のトレースバックを非表示にすることも可能です。
もちろんその場合も__context__には自動的に元の例外が保存されているため、デバッグすることは出来ます。

https://docs.python.org/ja/3.12/library/exceptions.html#exception-context

Discussion