注:下記のコードは、raise from
構文を使っているのでPython 3.3でしか動きませんが、基本的な話はPython 2.7でも変わりません。
今回は、Pythonの例外クラスのインスタンスを直接、例外クラスの引数にするのはマズいという話をしたいのですが、そもそも
「 例外クラスのインスタンスを直接、例外クラスの引数にする 」という状況がよく分からない。
という方もいらっしゃると思いますので、少し具体的な状況を設定してみます。
まず、Pythonでspamモジュールを作成して、モジュール例外の基底クラスとしてSpamError
を用意し、SpamError
とPython標準のIndexError
を継承したカスタム例外クラスSpamIndexError
を作ったことにします。
実際に書くとこんな感じですね。
class SpamError(Exception):
pass
class SpamIndexError(SpamError, IndexError):
pass
次に、spamモジュール内で発生したPython標準のエラーを、spamモジュールを利用するユーザーが、except spam.SpamError as exception:
等で補足できるようにしたいという要件のために、spamモジュール内で
try:
[][0] # IndexErrorが発生
except IndexError as original:
raise SpamIndexError(original) from original
のようなコードを書いたことにします。
最後の行でSpamError
の引数がexcept文で補足した例外であるoriginal
になっているのは、元のIndexError
と同等のメッセージを表示したいという意図があります。
これは、実際にあちこちのソースでよく見かけるやりかたです。
上記のコードを実行すると、
Traceback (most recent call last):
File "./spam.py", line 16, in <module>
[][0] # IndexErrorが発生
IndexError: list index out of range
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "./spam.py", line 18, in <module>
raise SpamIndexError(original) from original
spam.SpamIndexError: list index out of range
のように表示されます。
IndexError
もSpamIndexError
も list index out of range
と表示されていますね。
ここまでは問題ありません。
しかし、これがもしIndexError
ではなくKeyError
だった場合は残念ながら、
Traceback (most recent call last):
File "./spam.py", line 16, in <module>
{}[0] # KeyErrorが発生
KeyError: 0
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "./spam.py", line 18, in <module>
raise SpamKeyError(original) from original
spam.SpamKeyError: KeyError(0,)
のような表示になってしまいます。
元のKeyError
は、KeyError: 0
と表示されますが、SpamKeyError
はspam.SpamKeyError: KeyError(0,)
と表示されていますね。
IndexError
は引数をstr()
で評価しますが、KeyError
は引数が1つの場合はrepr()
で評価するのでこのようになります。
これを回避するには、
raise SpamKeyError(original) from original
の行を、
raise SpamKeyError(*original.args) from original
と書き換えて、original
の代わりに、元の例外の引数である、*original.args
を引数にします。
そうすると、
Traceback (most recent call last):
File "./spam.py", line 16, in <module>
{}[0] # KeyErrorが発生
KeyError: 0
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "./spam.py", line 18, in <module>
raise SpamKeyError(*original.args) from original
spam.SpamKeyError: 0
のように表示され、SpamKeyError
も元のKeyError
と同じように表示されます。
ただし、*original.args
を引数にしても問題ないのは、raiseする例外が、元の例外のクラスを継承している時だけです。
そうでない場合は、
raise SpamError(str(original)) from original
のように、補足した例外をstr()
で括って強制的に文字列を引数にするのが無難です。
単にException
クラスを継承しただけの例外クラスをraiseするのであれば、
raise SpamError(original) from original
のようにstr()
で括らなくても構わないと言えば構わないのですが、その場合は、KeyError: 0
と表示されていた例外が、spam.SpamError: 0
と表示されますので、どういうエラーなのかがメッセージから分かりづらくなります。
いずれにせよ、例外クラスのインスタンスであるoriginal
だけを、そのまま引数にするのは良くないということになるのでしょうね。