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やさらなる例外が発生したら、スタックして次に行く、という感じ。

変数の変更

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

programming/python/tips/try_except.txt · 最終更新: 2022/03/23 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0