LoginSignup
12
16

More than 5 years have passed since last update.

Pythonの例外クラスのインスタンスを直接、例外クラスの引数にするな!

Last updated at Posted at 2013-08-25

注:下記のコードは、raise from構文を使っているのでPython 3.3でしか動きませんが、基本的な話はPython 2.7でも変わりません。

今回は、Pythonの例外クラスのインスタンスを直接、例外クラスの引数にするのはマズいという話をしたいのですが、そもそも

例外クラスのインスタンスを直接、例外クラスの引数にする 」という状況がよく分からない。

という方もいらっしゃると思いますので、少し具体的な状況を設定してみます。

まず、Pythonでspamモジュールを作成して、モジュール例外の基底クラスとしてSpamErrorを用意し、SpamErrorとPython標準のIndexErrorを継承したカスタム例外クラスSpamIndexErrorを作ったことにします。

実際に書くとこんな感じですね。

spam.py
class SpamError(Exception):
    pass


class SpamIndexError(SpamError, IndexError):
    pass

次に、spamモジュール内で発生したPython標準のエラーを、spamモジュールを利用するユーザーが、except spam.SpamError as exception:等で補足できるようにしたいという要件のために、spamモジュール内で

spam.py
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

のように表示されます。
IndexErrorSpamIndexErrorlist 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と表示されますが、SpamKeyErrorspam.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だけを、そのまま引数にするのは良くないということになるのでしょうね。

12
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
16