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で済んでしまう。

