先日仕事で下記のような Python のコードを書いた。
try:
# 例外が発生しうる何らかの処理
except Exception as exc:
logger.exception("Something goes wrong..")
raise exc
この実装は一度例外をインターセプトして独自のエラーメッセージを出力し、 その上で元の例外を上げ直すという動作を期待した、所謂ありふれたコードだ。
だが、レビューアーからは「その書き方だとトレースバックが上書きされてしまうので、
raise だけで良いよ」というようなコメントをいただいた。
本投稿ではこの学びを検証し、
また Python3 から使用できるようになった raise ... from ... の場合なども合わせて簡単な検証を行おうと思う。
結論
先に結論を記載しておく。
raise excで元の例外オブジェクトを例外として挙げる方法でも Python3 であればスタックトレース内に元の例外発生箇所が含まれる。- ただし、
raise excしている箇所の情報も出力される
- ただし、
raiseだけを記載する方法は Python2 の時から有効な書き方で、スタックトレースもスマートに出力されるraise ... from ...はraiseのみ記載した時と同様、スマートなスタックトレースが出力されるため、例外種別を切り替えたいときは積極的に利用していきたい記法であるraise ... from Noneは元の例外のスタックトレースを完全に上書きしてしまうので、かなり利用箇所が限定されるものと思う。- もうサポートが切れているので書くことは無いと願っているが、もしも Python2 で実装をする機会があれば、このようなシチュエーションにおいては
raise excするのではなくraiseだけにしておくべきである。さもないと元の例外がどこで発生したのかという情報を失うこととなる。
検証するコード
import sys
# 検証1
def pattern_1():
try:
raise ValueError("エラーはここで発生")
except Exception as exc:
raise exc
# 検証2
def pattern_2():
try:
raise ValueError("エラーはここで発生")
except Exception:
raise
# 検証3
def pattern_3():
try:
raise ValueError("エラーはここで発生")
except Exception as exc:
raise Exception("新しいエラー") from exc
# 検証4
def pattern_4():
try:
raise ValueError("エラーはここで発生")
except Exception:
raise Exception("新しいエラー") from None
if __name__ == "__main__":
patterns = {
"p1": pattern_1,
"p2": pattern_2,
"p3": pattern_3,
"p4": pattern_4,
}
patterns[sys.argv[1]]()
とりあえず上記 4 つのパターンを検証することにする。
Python 3.9.4
検証1
検証1は raise exc したときのスタックトレースを確認する
$ python patterns.py p1
# ->
# Traceback (most recent call last):
# File "/home/k.s.k./Documents/misc/tmp/patterns.py", line 38, in <module>
# patterns[sys.argv[1]]()
# File "/home/k.s.k./Documents/misc/tmp/patterns.py", line 8, in pattern_1
# raise exc
# File "/home/k.s.k./Documents/misc/tmp/patterns.py", line 6, in pattern_1
# raise ValueError("エラーはここで発生")
# ValueError: エラーはここで発生
どうやら raise ValueError("エラーはここで発生") という箇所 (line 6) もスタックトーレスに含まれるようである。
しかし line 8 で raise exc したという情報もスタックトレースに記載される。
検証2
検証2はレビューでおすすめされた記法である。このときのスタックトレースを確認する
$ python patterns.py p2
# ->
# Traceback (most recent call last):
# File "/home/k.s.k./Documents/misc/tmp/patterns.py", line 38, in <module>
# patterns[sys.argv[1]]()
# File "/home/k.s.k./Documents/misc/tmp/patterns.py", line 13, in pattern_2
# raise ValueError("エラーはここで発生")
# ValueError: エラーはここで発生
検証2のスタックトレースには raise で例外を再度発生させたという痕跡が残っておらず、
元のスタックトレースがそのまま出力されているかのようである。
影響を与えず例外をインタラプト出来ているという点で検証2のやり方はスマートである。
検証3
こちらは例外チェーンというやり方である。
raise の後に何かを記載することでスタックトレースの表記が上書きされてしまうのであれば、
Python3 で登場したこの例外チェーンは検証2のようなスマートなスタックトレースを出力する事が出来ないんだろうか。
$ python patterns.py p3
# ->
# Traceback (most recent call last):
# File "/home/k.s.k./Documents/misc/tmp/patterns.py", line 20, in pattern_3
# raise ValueError("エラーはここで発生")
# ValueError: エラーはここで発生
#
# The above exception was the direct cause of the following exception:
#
# Traceback (most recent call last):
# File "/home/k.s.k./Documents/misc/tmp/patterns.py", line 38, in <module>
# patterns[sys.argv[1]]()
# File "/home/k.s.k./Documents/misc/tmp/patterns.py", line 22, in pattern_3
# raise Exception("新しいエラー") from exc
# Exception: 新しいエラー
検証3のスタックトレースでは元の例外情報も表示しつつ、
元の例外によって raise Exception("新しいエラー") が引き起こされたという出力になる。
とても理解しやすい。
検証4
ドキュメントを読むと raise ... from None という書き方もあるらしい。
$ python patterns.py p4
# ->
# Traceback (most recent call last):
# File "/home/k.s.k./Documents/misc/tmp/patterns.py", line 38, in <module>
# patterns[sys.argv[1]]()
# File "/home/k.s.k./Documents/misc/tmp/patterns.py", line 29, in pattern_4
# raise Exception("新しいエラー") from None
# Exception: 新しいエラー
こちらは元の例外情報を完全に上書きしており、
用意した4つの検証の中で唯一 raise ValueError("エラーはここで発生") に対する記載がなかった。
どういうシチュエーションでここまで元のエラーを秘匿したいという要求が出るのかピンとこないが、
きっとどこかで (それこそ自分のコードをある程度汎用的なモジュールとして切り出す時とか?) こういう書き方が求められるんだろう。
Python 2.7.18
今となってはもうサポートの切れてしまった Python2 だが、
当時の例外とスタックトレースがどういう挙動になっていたのかも確認しておく。
なお、例外チェーンはシンタックスとして存在しないため検証3、4はコメントアウトしておく。
また中に日本語を含めているのでファイルの頭に -*- encoding:utf-8 -*- という懐かしいおまじないも追記する。
検証1
$ python patterns.py p1
# ->
# Traceback (most recent call last):
# File "patterns.py", line 40, in <module>
# patterns[sys.argv[1]]()
# File "patterns.py", line 10, in pattern_1
# raise exc
# ValueError: エラーはここで発生
raise exc している箇所はスタックトレースに表示される。
また元の ValueError: エラーはここで発生 も表示される。
だが、肝心の ValueError が発生した場所に関する出力が存在しない。。
これだとエラーログを見ても実際にはどの箇所で例外が上がったのかを特定するのが難しいケースも出てきそうである。
検証2
$ python patterns.py p2
# ->
# Traceback (most recent call last):
# File "patterns.py", line 40, in <module>
# patterns[sys.argv[1]]()
# File "patterns.py", line 15, in pattern_2
# raise ValueError("エラーはここで発生")
# ValueError: エラーはここで発生
こちらは Python3 のときと同様、とてもスマートなスタックトレースが出力されている。