概要
今までPythonでコードを書くときに例外をなんとなく雰囲気で使っていたので、ちゃんと仕様を調べてみた。
検証したこと
1. Exceptionはコンストラクタ引数をargs
フィールドに保持している
以下のようなコードを例にする。
def hoge(): raise Exception("hoge") try: hoge() except Exception as e: print(e.args)
実行すると以下のような結果になる。
$ python main.py ('hoge',) $
次にExceptionの引数を以下のように変えて実行してみる。
raise Exception("hoge", "fuga")
実行すると以下のような結果になる。
$ python main.py ('hoge', 'fuga') $
以上から、Exceptionはコンストラクタ引数をフィールドにTupleで保持していることが分かる。
公式ドキュメントの該当箇所
公式ドキュメントでは以下にargs
の記載がある。
docs.python.org
これを読むと、args
はExceptionのさらに親クラスのBaseException
に定義されていることが分かる。
キーワード引数はargs
フィールドには保存されない
まず前提として、元のExceptionクラス自体はキーワード引数を取ることができない。
例えば、以下のようなコードを実行するとTypeError: Exception() takes no keyword arguments
が送出される。
def hoge(): raise Exception(message="hoge") hoge()
次に、以下のように、独自例外を定義してキーワード引数を受け取る場合を検証する。
class HogeException(Exception): def __init__(self, message1, message2): pass def hoge(): raise HogeException("hoge", message2="fuga") try: hoge() except Exception as e: print(e.args)
上記のコードを実行すると以下のような結果になる。
$ python main.py ('hoge',) $
以上から、キーワード引数はargs
には保存されないことが分かる。
2. スタックトレースにはargs
に保持している値が出力される
次に、スタックトレースには何が表示されるのかということをみていく。
以下のようなコードを例にする。
import logging logger = logging.getLogger(__name__) class HogeException(Exception): pass def hoge(): raise HogeException("hoge_message") try: hoge() except Exception: logger.exception("error!")
※ logger.exception
は巷のサンプルコードによく使い方の誤りがあるので、以下の記事を読んだ方がよい。
qiita.com
実行すると以下のようにHogeException: hoge_message
が出力される。
$ python main.py error! Traceback (most recent call last): File "/path/to/main.py", line 15, in <module> hoge() File "/path/to/main.py", line 11, in hoge raise HogeException("hoge_message") HogeException: hoge_message $
次に以下のように引数を追加してみる。
raise HogeException("hoge_message", "fuga_message")
今度は結果は以下のようになる。
error! ... HogeException: ('hoge_message', 'fuga_message')
上記の実行結果より、スタックトレースには以下の仕様でエラーが表示されていると推測される。
- 「${例外クラス名}: ${
args
}」が表示される。 args
のTulpleの要素が1個か2個以上かで表示が少し変わる。(括弧が付くか否か)
一応、検証としてargs
の値を書き変えた場合の実行結果を確認してみる。
def hoge(): raise HogeException("hoge_message") try: hoge() except Exception as e: e.args = ("fuga_message",) logger.exception("error!")
実行するとHogeException: fuga_message
が出力されたので、推測は当たっているようである。
3. スタックトレースにエラーメッセージを出力する際は__str__
が呼ばれている
2.にて、スタックトレースにはargs
の値が出力されることが分かった。
公式ドキュメントを読むと、args
は__str__
を呼ぶことでも出力できるということが記載されている。
例外インスタンスには __str__() が定義されており、 .args を参照しなくても引数を直接印字できるように利便性が図られています。
この時に、スタックトレースにエラーメッセージを出力する際も直接args
を参照しているのではなく、__str__
を呼んでいるのではないかと思った。
検証のため、以下のコードを実行してみる。
import logging logger = logging.getLogger(__name__) class HogeException(Exception): def __init__(self, message1, message2): self.message1 = message1 self.message2 = message2 def __str__(self) -> str: return f"==={self.message2}===" def hoge(): raise HogeException("hoge_message", message2="fuga_message") try: hoge() except Exception as e: logger.exception("error!")
結果は予想通り、HogeException: ===fuga_message===
が表示された。
以上より、スタックトレースにエラーメッセージを出力する際は__str__
経由でargs
が出力されることが分かった。
__repr__
は呼ばれない
念のため、上のコードで__str__
の代わりに__repr__
を定義した場合も検証してみた。
class HogeException(Exception): def __init__(self, message1, message2): self.message1 = message1 self.message2 = message2 def __repr__(self) -> str: return f"==={self.message2}==="
この場合はHogeException: hoge_message
が表示されたので、__repr__
は呼ばれていないということが分かった。
4. 独自のエラーメッセージを出力する方法
1.〜3.を踏まえて、スタックトレースに独自のエラーメッセージを出力する方法を検討する。
以下の2つのやり方があることが分かる。
- 親の例外クラスのコンストラクタに渡す。
__str__
を定義する。
1. 親の例外クラスのコンストラクタに渡す。
という仕様を使って、親の例外クラスのコンストラクタに独自のエラーメッセージを渡すことで、実現することができる。
コードは以下のようになる。
import logging logger = logging.getLogger(__name__) class HogeException(Exception): def __init__(self, message): super().__init__(f"==={message}===") def hoge(): raise HogeException(message="hoge_message") try: hoge() except Exception as e: logger.exception("error!")
実行すると想定通り以下のような結果になる。
error! Traceback (most recent call last): File "/path/to/main.py", line 16, in <module> hoge() File "/path/to/main.py", line 12, in hoge raise HogeException(message="hoge_message") HogeException: ===hoge_message===
例えば、urllib3
の独自例外はこの方法を使っている。
class PoolError(HTTPError): """Base exception for errors caused within a pool.""" pool: "ConnectionPool" def __init__(self, pool: "ConnectionPool", message: str) -> None: self.pool = pool super().__init__(f"{pool}: {message}")
2. __str__
を定義する。
こちらは検証時にコードも示しているので、コードは省略する。
例えば、SQLAlchemy
の独自例外はこの方法を使っている。
class SQLAlchemyError(HasDescriptionCode, Exception): """Generic error class.""" def _message(self, as_unicode=compat.py3k): if len(self.args) == 1: text = self.args[0] if as_unicode and isinstance(text, compat.binary_types): text = compat.decode_backslashreplace(text, "utf-8") elif compat.py3k or not as_unicode: text = str(text) else: text = compat.text_type(text) return text else: return str(self.args) def _sql_message(self, as_unicode): message = self._message(as_unicode) if self.code: message = "%s %s" % (message, self._code_str()) return message def __str__(self): return self._sql_message(compat.py3k) def __unicode__(self): return self._sql_message(as_unicode=True)
どちらの方法でも良さそうだが、状況に応じてどっちの方がベターとかあるのであれば知りたい。
以上