nikkie-ftnextの日記

イベントレポートや読書メモを発信

Pythonで例外を文字列中に出力するときは、str()ではなくてrepr()に渡すのがオススメです

目次

百聞は一見に如かず(タイトル回収パート)

例外をstr()とrepr()に渡して返り値の文字列を比較しよう

Python 3.11.4で確認したコードです。

>>> ex = Exception("yabaiyo")
>>> str(ex)
'yabaiyo'
>>> repr(ex)
"Exception('yabaiyo')"

Pythonチュートリアルの「7.1. 出力を見やすくフォーマットする」より

str() 関数は値の人間に読める表現を返すためのもの

repr() 関数はインタープリタに読める (略) 表現を返すためのもの

チュートリアルの「8.3. 例外を処理する」より

利便性のため、組み込み例外型には __str__() が定義されており、明示的に .args を参照せずとも すべての引数を表示できます。

__str__()str()で呼び出されます1

argsはBaseException(全ての組み込み例外の基底クラス。Exceptionの親クラス)に定義された属性。
https://docs.python.org/ja/3/library/exceptions.html#BaseException.args

例外コンストラクタに与えられた引数のタプルです。

BaseExceptionのドキュメントより
https://docs.python.org/ja/3/library/exceptions.html#BaseException

このクラスのインスタンスに str() が呼ばれた場合、インスタンスへの引数の表現か、引数が無い場合には空文字列が返されます。

ここの「引数」はargs属性を指していると私は理解しました。

違いが顕著な例:何も渡さずに例外を初期化する

Pythonの例外は文字列を渡さずに初期化もできます。
このとき、strとreprの違いが露骨に出ます

>>> ex2 = Exception()
>>> str(ex2)
''
>>> repr(ex2)
'Exception()'

str()の返り値は空文字列ですね

print()についてもreprを介しましょう

print()で例外を直接出力する場合も同様です

>>> ex2 = Exception()
>>> print(ex2)

>>> print(str(ex2))

>>> print(repr(ex2))
Exception()

tips:f-stringは!rでrepr

f-stringで!rを指定するとreprの扱いとなります

>>> ex = Exception("yabaiyo")
>>> f"Raised {ex}"
'Raised yabaiyo'
>>> f"Raised {ex!r}"
"Raised Exception('yabaiyo')"

f-stringについてはこちらの記事が詳しく、とても参考になります。

f-stringのドキュメントによると!r変換なるもの。
https://docs.python.org/ja/3/reference/lexical_analysis.html#formatted-string-literals

変換 '!s' は str() を、 '!r' は repr() を、そして '!a' は ascii() を呼び出します。

f"{ex!r}"という書き方は『Python Distilled』にもシュッと登場しました(3.4)。

print(f"An error occurred : {e!r}")

執筆の背景:str()で出力したために小さくやらかした

どのような具体の例外が送出されるかがよくわかっていないためにexcept Exception:というコードを書きました2

try:
    # 例外を送出しうるコード
except Exception as ex:
    print(f"Raised {ex}")

このprintは実行されたのですが、

Raised 

というログ🙄
どんな例外か分からないからprintした3のに、例外をreprに渡していなかったばっかりに結局どんな例外か分かりませんでした。
📣俺みたいになるな!

まとめ

Pythonで例外を文字列中に出力するときはrepr()を使いましょう。

try:
    raise RuntimeError()
except Exception as ex:
    print(f"Raised {ex!r}")

repr()を使わない(未指定・またはstr()を使う)場合、例外のargsしか文字列中に表示されません。
argsがない場合に空文字列が返り、例外についての情報が全く確認できなくなってしまいます。

P.S. loggingの場合もrepr()の返り値を%sへ渡そう

手短に動作確認しました4

>>> import logging
>>> ex2 = Exception()
>>> logging.warning("Raised %s", ex2)
WARNING:root:Raised
>>> logging.warning("Raised %s", str(ex2))
WARNING:root:Raised
>>> logging.warning("Raised %s", repr(ex2))
WARNING:root:Raised Exception()

  1. repr()が呼び出すのは__repr__()です。この前後をどうぞ https://docs.python.org/ja/3/reference/datamodel.html#object.__str__
  2. 参考
  3. printを使っている理由としては、DockerイメージにしてKubernetesのPodとして動かしていて、ロガーを使わずにprintだけでもPodのログとして見えるためです
  4. この記事では動作確認優先でルートロガーを使っていますが、私の意見としては本番コードにはルートロガーではなく子のロガーでログ出力を書きたいです。ルートロガーを使っていることの参考