LoginSignup
10
12

More than 5 years have passed since last update.

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

Last updated at Posted at 2013-08-17

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

一般的な方法

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

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

urllib83/__init__.py
         ftp.py
         http.py

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

__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.FTPErrorurllib83.HTTPErrorという例外クラスが存在し、どちらのクラスもurllib83.Urllib83Errorというベース例外クラスを継承しているということにします。
これら3つの例外クラスを素直に実装すると、urllib83/__init__.py内で、

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


class FTPError(Urllib83Error):
    pass


class HTTPError(Urllib83Error):
    pass

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

urllib83/ftp.py
from . import FTPError

def ftpopen(url):
    ...
    raise FTPError

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

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

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

class FTPError(Urllib83Error):
    pass

class HTTPError(Urllib83Error):
    pass

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

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

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

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

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

client.py
from urllib83 import urlopen, Urllib83Error

try:
    urlopen(url)
except Urllib83Error:
    pass

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

何が問題なのか?

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

>>> 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で例外クラスを

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__属性の内容を一時的に書き換えて、例外クラスの定義が終わったら元に戻すことです。
例外が発生すると以下のように表示されます。

>>> 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__属性を元に戻すのを忘れないようにしてください。

最後に

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

10
12
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
10
12