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

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

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

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

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

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

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

```py3:spam.py
class SpamError(Exception):
    pass


class SpamIndexError(SpamError, IndexError):
    pass
```

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

```py3:spam.py
try:
    [][0]  # IndexErrorが発生
except IndexError as original:
    raise SpamIndexError(original) from original
```

のようなコードを書いたことにします。
最後の行で`SpamError`の引数がexcept文で補足した例外である`original`になっているのは、元の`IndexError`と同等のメッセージを表示したいという意図があります。
これは、実際にあちこちのソースでよく見かけるやりかたです。

上記のコードを実行すると、

```py3:
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`だった場合は残念ながら、

```py3:
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()`で評価するのでこのようになります。

これを回避するには、

```py3:
    raise SpamKeyError(original) from original
```

の行を、

```py3:
    raise SpamKeyError(*original.args) from original
```

と書き換えて、`original`の代わりに、元の例外の引数である、`*original.args`を引数にします。
そうすると、

```py3:
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する例外が、元の例外のクラスを継承している時だけです。
そうでない場合は、

```py3:
    raise SpamError(str(original)) from original
```

のように、補足した例外を`str()`で括って強制的に文字列を引数にするのが無難です。

単に`Exception`クラスを継承しただけの例外クラスをraiseするのであれば、

```py3:
    raise SpamError(original) from original
```

のように`str()`で括らなくても構わないと言えば構わないのですが、その場合は、`KeyError: 0`と表示されていた例外が、`spam.SpamError: 0`と表示されますので、どういうエラーなのかがメッセージから分かりづらくなります。

いずれにせよ、例外クラスのインスタンスである`original`だけを、そのまま引数にするのは良くないということになるのでしょうね。