注:下記のコードは、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だけを、そのまま引数にするのは良くないということになるのでしょうね。