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