Pythonの例外処理に関するまとめ


このエントリーをはてなブックマークに追加

適当に書いてしまいがちな例外処理について自分なりにまとめました。

Pythonにおける例外

Pythonでは「認可をとるより許しを請う方が容易 (easier to ask for forgiveness than permission)」、略してEAFPというコーディングスタイルが推奨されています。EAFPは、エラーを起こすかもしれない処理もまず実行してみて、もしエラーが出たらそのとき後始末をする、というスタイルです。従って、Pythonは比較的例外処理が使われやすい言語であると言えます。

基本的な文法

0除算エラーと型エラーを捕捉する例を以下に示します。

def func(val):
    try:
        res = 1 / val
    except ZeroDivisionError:
        print("zero division error")
    except TypeError:
        print("type error")
    else:
        print("1 / {} = {}.".format(val, res))
    finally:
        print("end of func()")

func(1)
func(0)
func(None)

各節の意味は以下の通りです。

  • try節
    • 例外が発生する可能性があるコードを書くところです。
  • except節
    • 例外クラス名を指定し、捕捉する例外の種類を限定して捕捉します。何個も並べることもできます。
  • else節
    • try節内で例外が発生しなかった場合のみ実行される節です。
  • finally節
    • try節内で例外が発生したか否かに関わらず実行される節です。

実行結果は以下です。

1 / 1 = 1.0.
end of func()
zero division error
end of func()
type error
end of func()

例外オブジェクトを捕捉するにはasを使います。

try:
    1/0
except ZeroDivisionError as e:
    print(e)

実行結果は以下です。

division by zero

すべての例外を捕捉する

発生する例外の種類がわかっているときは、前節で書いたように、なるべく具体的に例外の種類を指定するのがよいとされています(要出典)。しかし実際には、どんな例外を発生するかわからないコードを呼び出す必要に迫られることがあります。このときには発生しうるすべての例外を捕捉したくなるでしょう。よく見かけるがあまりお勧めでない書き方を以下に示します。

try:
    dirty_func()
except:
    print("error!")

この書き方には2つの問題があると言えます。

  1. 捕捉したくない例外まで捕捉されてしまう
  2. 重要なエラーを揉み消してしまう

順番に見ていきます。

問題1. 捕捉したくない例外まで捕捉されてしまう

このコードの1つ目の問題は、Ctrl+CによるKeyboardInterruptまでをも捕捉してしまうことです(もちろん、それが意図したことならそれでよいのですが)。例えば以下のコード

sum = 0
while True:
    try:
        sum += 1
    except:
        print("error!")

を実行し、Ctrl+Cを押してみてください。ちょうどsum += 1の実行中にCtrl+Cを押すとそれが例外として捕捉されてしまうので、複数回Ctrl+Cを押さないと終了できないことがあると思います。

改善したコードを以下に示します。こうすると、KeyboardInterruptSystemExitなど捕捉したくないいくつかの例外を除いた(ほぼ)すべての例外が捕捉できます。

try:
    dirty_func()
except Exception as e:
    print(e)

この理由は5. 組み込み例外 — Python 3.5.2 ドキュメントを見ると分かります。このページのExceptionクラスの説明には「システム終了以外の全ての組み込み例外はこのクラスから派生しています。全てのユーザ定義例外もこのクラスから派生させるべきです」とあります。つまり、まともなライブラリが返す例外は、すえてExceptionクラスを継承していることが期待されるので、上記のコードでうまくいくわけです。

ただし、可能性は低いですが、Exceptionクラスを継承せずに作成した行儀の悪いユーザ定義例外を返されてしまうと、上記コードでは捕捉できません。例を以下に示します。

# 誤った例外クラス定義の方法! 正しくはExceptionクラスを継承してください
class MyError(BaseException):
    pass

try:
    raise MyError
except Exception as e:
    print("error!")

これを実行するとエラー捕捉に失敗することが確かめられます。このようなレアな現象まで考慮せざるを得ない場合は、except Exception:ではなくexcept:と書かないといけないはずです。

問題2. 重要なエラーを揉み消してしまう

例外をキャッチして適切な処理をせずそのまま揉み消すくらいなら何もしない方がマシなことがあります。例えば以下のコードは0から9までの和を計算するつもりのコードですが、実行すると0が表示されます。

ans = 0
for i in range(10):
    try:
        ams += i
    except Exception:
        pass
print(ans)

これは、ansをミススペルしたamsによりNameErrorが発生したのにも関わらず、それを揉み消してしまったせいで起こった悲劇です。プログラムは動作し続けるものの、どんな挙動になるか予想ができない危険な状態と言えます。

改善案として、except節にてraiseにより例外を投げ直すという手があります。コードを以下に示します。

ans = 0
for i in range(10):
    try:
        ams += i
    except Exception:
        raise
print(ans)

これを実行すると、以下のように例外が出てプログラムが停止します。

Traceback (most recent call last):
  File "error_test.py", line 9, in <module>
    ams += i
NameError: name 'ams' is not defined

このように、exceptチェーンの最後ではraiseと書いておくのがよいと思います。

どんな怪しい状態になってもいいからどにかくプログラムを停止させたくない、という場合ではraiseと書きたくないかもしれません。このような場合であっても、最低限なんらかのログを残しておくことくらいはしておくべきだと思います。

スタックトレースを表示

例外処理をするとスタックトレースが表示されなくなってしまうので、デバッグのときに不便です。例えば以下のコードを実行すると、division by zeroとしか表示されず、エラー箇所がわかりません。

def func1():
    1 / 0

def func2():
    return func1()

try:
    func2()
except Exception as e:
    print(e)

スタックトレースを表示するには、標準ライブラリのtracebackを使います。

import traceback

def func1():
    1 / 0

def func2():
    return func1()

try:
    func2()
except Exception as e:
    traceback.print_exc()

上記コードを実行すると、以下のように例外発生箇所が分かります。

Traceback (most recent call last):
  File "error_test.py", line 26, in <module>
    func2()
  File "error_test.py", line 23, in func2
    return func1()
  File "error_test.py", line 20, in func1
    1 / 0
ZeroDivisionError: division by zero

参考URLs