Pythonの例外処理のfinally
Pythonでは、例外処理の「try」「except」節の他に、成功したときの「else」節、常に実行する「finally」節がある。
try: a = 1 // 0 # ZeroDivisionError except: # エラー時処理 pass else: # 成功時処理 pass finally: # 常に実行 pass
が、finally節の挙動は、ちょっと癖がある。癖というか、別にPython特有の挙動でもなく他の言語でも同じような挙動をするし、以下の様々なケースに対応してたらそうなったという感じだが。
- tryの中でreturnされたら実行するの?
- exceptやelseの中でreturnされたら実行するの?
- exceptやelseの中でさらに例外が発生したら実行するの?その場合、その例外はどうなるの?
まぁ、冒頭にリンク張った公式ドキュメント読めばいいのだが、基本的には「ほぼ実行される」。
try,except/else,finallyの各節について、結果は大きく「最後まで上手くいったとき」「途中で例外が発生したとき」「途中でreturn,continue,breakなど、離れたところに飛ぶ命令に到達したとき」の3点に分けられる。それぞれについて場合分けすると、以下のようになる。
●tryで例外Aが発生 | |- exceptでキャッチされた | →該当するexcept実行 | | | |- exceptで例外Bが発生 | | →それを【暗黙的にキャッチ、保留しておき】、finally実行 | | | | | |- finallyで例外Cが発生 | | | ○ Cをraise(スタックトレースにはB,Aの情報もある) | | | | | |- finallyが無事終了 | | | ○ Bをraise(スタックトレースにはAの情報もある) | | | | | |- finally内でreturn,break,continueに到達 | | ○ そのままreturn/break/continueが処理され、【例外Bは送出されない】 | | | |- exceptが無事終了 | | → finally実行 | | | | | |- finallyで例外Cが発生 | | | ○ Cをraise(スタックトレースにAの情報はない) | | | | | |- finallyが無事終了、またはreturn,break,continueに到達 | | | ○ その通りに処理され、継続 | | | |- except内でreturn,break,continueに到達 | → その直前にfinallyを実行 | | | |- finallyで例外Cが発生 | | ○ Cをraise(スタックトレースにAの情報はない) | | | |- finallyが無事終了、またはreturn,break,continueに到達 | | ○ その通りに処理され、継続 | | 【returnの場合、返される値は、finally節によるものになる】 | |- exceptでキャッチされなかった(Aに合致するエラーがなかった) | →finally実行 | | | |- finallyで例外Cが発生 | | ○ Cをraise(スタックトレースにAの情報もある) | | | |- finallyが無事終了 | | ○ Aをraise | | | |- finally内でreturn,break,continueに到達 | ○ そのままreturn/break/continueが処理され、【例外Aは送出されない】 | ●tryが無事終了 | |- else節が存在 | →else実行 | (上記のexcept節をelse節に読み替えたものと、例外Aが存在しない以外だいたい同じ) | |- else節はない | →finally実行 | | | |- finallyで例外Cが発生 | | ○ Cをraise | | | |- finallyが無事終了、またはreturn,break,continueに到達 | | ○ その通りに処理され、継続 | ●try内でreturn,break,continueに到達 | →その直前にfinally実行。else節は実行されない | | | |- finallyで例外Cが発生 | | ○ Cをraise | | | |- finallyが無事終了 | | ○ tryのreturn,break,continueが処理される | | | |- finally内でreturn,break,continueに到達 | ○ finallyのreturn/break/continueが処理される 【returnの場合、返される値は、finally節によるものになる】
特に【】をつけた部分が、注意を要する挙動かなと思う。
あくまで処理の順番は try→(except/else)→finally で、その過程でreturnやさらなる例外が発生したら、スタックして次に行く、という感じ。
変数の変更
- ↑はJavaだが挙動は同じ
try,except,else内のreturnに対しては、数値型やbooleanなどプリミティブな型の変数は、finally節での変更が反映されない。
def test(): val = 0 try: a = 1 // 0 except: return val finally: val += 5 print(test()) # => 0
この辺の挙動は関数の引数と同様。リストなど参照型の変数なら、反映される。
def test(): val = [0] try: a = 1 // 0 except: return val finally: val[0] = 5 print(test()) # => [5]
使いどころ
一般的にfinallyは、開いたリソースやコネクションを必ず解放する、などの目的で使用されることはあるけど、
Pythonならそういうのは基本的に with
構文を使った方がよい。
他の使いどころとしては、、、
- 処理中に複数の中間ファイルを生成するが、途中でエラーが発生したとしても、最後はきちんと削除し後片付けされた状態を保ちたい
- tryの外にスコープを持つ変数を、tryの中で更新するコードを書いたとして、上手くいったら結果を、仮に例外が発生しても途中までの結果を、printなり書き出しなりして確認したい
みたいな時かなあ。
大体の使いどころはwithで済んでしまう。