エラーは「機嫌の悪い値」みたいなもの
Pythonで何かがうまくいかないとき——ゼロ除算、存在しないファイルの読み込み、数値への変換失敗など——ランタイムは 例外 (exception) オブジェクトを生成し、誰かがそれをキャッチするまでコールスタックを遡っていきます。最後まで誰もキャッチしなければ、プログラムは停止してトレースバックを吐き出すわけです。
例外そのものは悪者ではありません。「この処理はもう続けられません。理由はこれです」とPythonが知らせてくれる仕組みです。ですから、どの例外は自分で復旧できて、どれはそのまま上に投げるべきか——これをケースバイケースで判断するのが、私たちの仕事になります。
python 例外処理の基本形
try:で、失敗するかもしれないコードのブロックを始めます。except ValueError:は、tryの中で該当する例外が発生したときだけキャッチします。- 例外が発生しなければ、
exceptの部分はまるごとスキップされます。
スニペットに 42 を渡せば普通に動きます。hello を渡すと、ハンドラ側が動くというわけです。
特定の例外だけをキャッチする
Python の例外クラスは階層構造になっています。よく出会うものをいくつか挙げておきます。
ValueError— 値そのものがおかしいケース(int("abc")や範囲外の引数など)。TypeError— 型が合っていないケース("hi" + 3など)。KeyError— 辞書に指定したキーが存在しないとき。IndexError— シーケンスのインデックスが範囲外のとき。FileNotFoundError— ファイルが見つからないとき。ZeroDivisionError— ゼロ除算をしたとき。AttributeError— オブジェクトに指定した属性が無いとき。
対処の仕方がわかっている例外だけをピンポイントでキャッチしましょう。
タプルで複数の例外をまとめて指定すれば、一つの except 節でまとめてキャッチできます。
as e の部分に注目してください。これで例外オブジェクトを e に束縛できるので、メッセージや属性を後から確認できます。
すべてをキャッチしてはいけない
except: だけで受けると、本当に何でもかんでもキャッチしてしまいます。KeyboardInterrupt(Ctrl-C による中断)やシステム終了まで巻き込むので、使わないでください。
except Exception: は少しマシですが、それでも危険です。想定外のバグまで飲み込んでしまい、本当の原因を隠してしまいます。
# Don't do this without a really good reason.
try:
do_something()
except Exception:
pass
基本的には「自分が対処できるとわかっている例外だけをキャッチする」のが正解です。想定外の例外がプログラムの最上位まで飛んでしまった場合でも、トレースバックを見れば何が起きたかが一目でわかります。これはバグではなく、むしろありがたい仕様です。
else と finally
try 文には、さらに2つのオプション節があります。
elseは、tryブロックが例外を発生させずに正常終了したときに実行されます。finallyは、例外の有無に関わらず必ず実行されます。
else は「成功したときだけ動かしたい処理」を書くのにぴったりな場所です。try ブロックには、本当に失敗する可能性のある処理だけを入れておきたいですよね。一方 finally は、try が失敗しても必ず実行してほしい後片付け用です。たとえばリソースのクローズ、ロックの解放、状態の復元などですね。
raise で例外を自分から投げる
自分のコードでエラーを知らせたいときは raise を使います:
何が起きたのかに合ったエラー型を選びましょう。まずは ValueError、TypeError、FileNotFoundError といった組み込みの例外から検討し、自作クラスに手を出すのはその後です。
カスタム例外を定義する
組み込みの例外では意味がうまく伝わらないときは、独自の例外クラスを用意します。
Exception(あるいはもっと具体的な組み込み例外)を継承して、クラスにdocstringを付けておく。基本的にはこれだけでOKです。カスタム例外を作っておくと、呼び出し側は「自分のドメインにとって意味のあるエラーだけ」をピンポイントでキャッチできます。
raise ... from ...:例外の連鎖
ある例外がきっかけで別の例外が発生したときは、その連鎖を残しておきましょう。
from e を付けると、元の例外が連鎖情報として紐づきます。トレースバックを出力したとき、表に出た ConfigError と、その原因となった FileNotFoundError の両方が表示されるわけです。デバッグのときにこの履歴があるのは本当に助かります。
コンテキストマネージャー:後始末をもっとスマートに
finally でも十分役目は果たせますが、ファイルのようなリソースを扱う場合は コンテキストマネージャー(with 文で使うあれです)のほうがほぼ間違いなく優れています。
# finally version
f = open("data.txt")
try:
data = f.read()
finally:
f.close()
# with version
with open("data.txt") as f:
data = f.read()
どちらでも安全です。with構文の方が短く書けて、後片付けも自動でやってくれます。finallyを持ち出すのは、標準ライブラリがコンテキストマネージャーでラップしてくれていない処理を自分で書くときだけにしましょう。
例外をキャッチすべきでないとき
例外をキャッチするというのは、「自分でこのエラーに対処できる」という 意思表示 です。対処できないなら、素直に上位へ伝播させましょう。次のようなコードは、ほぼ間違いなくアンチパターンです。
try:
do_work()
except Exception:
pass # silently ignore everything
エラーを握りつぶすと、バグが見えなくなります。中途半端な状態でダマシダマシ動かすくらいなら、派手にクラッシュさせたほうがマシです。
まとめ
try/exceptは、回復可能なエラーを処理するために使う。Exceptionでざっくり捕まえず、具体的な例外をキャッチする。- 自分のコードでエラーを知らせたいときは
raiseを使う。 finallyによるクリーンアップの多くは、withブロックで置き換えられる。- 迷ったら、例外はそのまま上に伝播させる。
次回は、Python でよく遭遇する具体的なエラーたち —— KeyError、ValueError、ModuleNotFoundError など —— を一通り見ていきます。あわせて、それらをサッと直すためのデバッグの習慣も紹介します。
よくある質問
Pythonでエラーを処理するにはどうすればいい?
失敗する可能性のある処理を try ブロックで囲み、except で具体的な例外を捕まえます。例えば try: risky() except ValueError: ... のように書きます。else は例外が出なかったときだけ実行され、finally は例外の有無に関わらず必ず実行されるので、後片付け処理に使います。
念のため Exception を全部キャッチしておけば安全では?
いいえ、それはやめた方がいいです。except: や except Exception: で何でも捕まえてしまうと、想定外のバグまで握りつぶしてしまいます。自分が対処できる例外だけを具体的にキャッチし、それ以外は伝播させて本当の問題に気付けるようにしましょう。
raise と raise from の違いは?
raise NewError(...) は新しい例外を発生させるだけですが、raise NewError(...) from original と書くと元の例外を「原因」として紐付けてくれて、トレースバックにも表示されます。低レベルのエラーを受けて、より分かりやすい高レベルの例外として投げ直したいときに from を使うのが定番です。