Pythonのモジュール例外の表示を分かりやすくする

ここ最近、Pythonのモジュール例外の表示を分かりやすくする方法について悩んでいたのですが、一番良さそうな方法が見つかったので、まとめてみました。
まずはモジュール例外を定義する一般的な方法をおさらいします。

## 一般的な方法 ##
ここでは具体的な状況として、Python 3.3で、`urllib83.urlopen()`という関数からなる、urllib83モジュールを書くという状況を考えてみます。
urllib83は、83番目のurllibモジュールという意味ですが、もし将来的に同名の標準モジュールが登場したらすみません。

それはさておき、このモジュールは下記の3ファイルで構成されていることにします。

```
urllib83/__init__.py
         ftp.py
         http.py
```

`urllib83.urlopen()`を思いっきり手抜きして書くと、こんな感じになるはずですね。

```py3:__init__.py
from .ftp import ftpopen
from .http import httpopen


def urlopen(url):
    if url.startswith("ftp://"):
        return ftpopen(url)
    else:
        return httpopen(url)
```

次に、このモジュールには`urllib83.FTPError`と`urllib83.HTTPError`という例外クラスが存在し、どちらのクラスも`urllib83.Urllib83Error`というベース例外クラスを継承しているということにします。
これら3つの例外クラスを素直に実装すると、`urllib83/__init__.py`内で、

```py3:urllib83/__init__.py
class Urllib83Error(Exception):
    pass


class FTPError(Urllib83Error):
    pass


class HTTPError(Urllib83Error):
    pass
```

のように定義することになると思いますが、このやり方には一つ問題があります。
`urllib83.FTPError`は、当然`urllib83/ftp.py`内でraiseされますので、`ftp.py`は`FTPError`を、

```py3:urllib83/ftp.py
from . import FTPError

def ftpopen(url):
    ...
    raise FTPError
```

のようにimportして利用しますが、前述したように`__init__.py`は`ftp.py`から`ftpopen()`をimportしています。
このままでは、`__init__.py`と`ftp.py`がお互いをimportして、循環インポートという問題が発生してしまうのです。

そこで、一般的にはこの問題を避けるために`urllib83/errors.py`というファイルを追加して、下記のような内容にします。

```py3:urllib83/errors.py
class Urllib83Error(Exception):
    pass

class FTPError(Urllib83Error):
    pass

class HTTPError(Urllib83Error):
    pass
```

`errors.py`以外のファイルはそれぞれ、

```py3:__init__.py```
from .errors import Urllib83Error, FTPError, HTTPError

__all__ = ['Urllib83Error', 'FTPError', 'HTTPError']
```

のように必要な例外クラスをインポートします。（注：`__all__`の行が必要なのは、`__init__.py`だけです。）

`__init__.py`も例外クラスをimportしていますので、urllib83モジュールを利用する側も、

```py3:client.py
from urllib83 import urlopen, Urllib83Error

try:
    urlopen(url)
except Urllib83Error:
    pass
```

のようにモジュール例外を使えます。
ここまでは何も問題ありません。

## 何が問題なのか？ ##
私が気になったのは、urllib83モジュールを使う側がtry...except文を使わずに、`urllib83.urlopen()`
を呼び出した場合です。
`urllib83.FTPError`が発生すると、トレースバックが下記のように表示されます。

```py3:
>>> import urllib83
>>> urllib83.urlopen("ftp://")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "./urllib83/__init__.py", line 10, in urlopen
    return ftpopen(url)
  File "./urllib83/ftp.py", line 4, in ftpopen
    raise FTPError
urllib83.errors.FTPError
>>>
```

最後の行で`urllib83.FTPError`ではなく、`urllib83.errors.FTPError`と表示されていますね。
これは問題であり、`urllib83.FTPError`と表示されるべきだというのが私の意見です。

しかし、Pythonistaの方々の一般的な見解では、「この表示のどこに問題が存在するのかさっぱり分からない」ということになるようです。その理由としては、

> urllib83モジュールのドキュメントには、`urllib83.FTPError` が発生すると記載されているだろうし、利用者がexcept句に、`except urllib83.FTPError:` と書けば発生した例外を補足できる。
> ドキュメントに書かれている仕様の通りに動作するのだから、問題はどこにも存在しない

とのことでした。一方、それに対する私の反論は、

> urllib83モジュールのユーザーが、`urllib83.errors.FTPError`という表示に実際に遭遇した時に、それがurllib83モジュールのドキュメントに記載された`urllib83.FTPError`であることを知るためのコストが高すぎるのではないか？
> 最初から、`urllib83.FTPError`と表示されたほうが分かりやすいし、ユーザーに親切なので、何とかすべきである

というものです。

## 解決方法 ##
`urllib83.FTPError`と表示されるようにする方法を色々試してみたのですが、結局`errors.py`で例外クラスを

```py3:errors.py
_SAVED_NAME = __name__
__name__ = __package__
#__name__ = "urllib83" # Python 3.2 まではこちらで


class Urllib83Error(Exception):
    pass


class FTPError(Urllib83Error):
    pass


class HTTPError(Urllib83Error):
    pass

__name__ = _SAVED_NAME
```

のように定義するのが一番良さそうだという結論になりました。

ポイントは、`__name__`属性の内容を一時的に書き換えて、例外クラスの定義が終わったら元に戻すことです。
例外が発生すると以下のように表示されます。

```py3:
>>> import urllib83
>>> urllib83.urlopen("ftp://")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "./urllib83/__init__.py", line 10, in urlopen
    return ftpopen(url)
  File "./urllib83/ftp.py", line 4, in ftpopen
    raise FTPError
urllib83.FTPError
>>>
```

ちゃんと`urllib83.FTPError`と表示されていますね。

`__name__`属性の書き換えは一般的ではないですし、複雑なコードを書くと、`urllib83/__init__.py` 内で定義した時と比べて違いが出てくることもありますので、`__name__`属性を書き換えている間は込み入った処理をできるだけ避けることを強くオススメします。
それから、`__name__`属性を元に戻すのを忘れないようにしてください。

## 最後に ##
今のところ、この方法で特に問題は見つかっていませんが、もし何か問題点に気がついた方は、ぜひコメント等でお知らせください。
個人的には、統合開発環境で何か問題が発生するのではないか？という点を特に心配しています。