ユーザーが入力した文字を数字に変換しようとしたら、いきなり赤い文字が出てプログラムが止まってしまった。そんな経験はありませんか?
Pythonでは、想定外のことが起きると例外と呼ばれる合図が出て、処理がその場で止まります。止まること自体は悪いことではありません。けれど、ちょっとした入力ミスのたびにアプリ全体が落ちてしまうのは困りますよね。
そこで使うのが、try文を使った例外処理です。try・except・else・finallyという4つのブロックを組み合わせて、エラーが起きたときの動きを自分で決められます。
今回は、この4つのブロックがどんな順番で動くのかを、実際に動かした結果とともに整理していきます。サンプルの実行結果は、手元のPython 3.13で確かめたものです。
そもそも例外とは何か¶
例外とは、プログラムの実行中に起きた異常を知らせる仕組みのことです。数字ではない文字をint()に渡したときのValueErrorや、0で割ったときのZeroDivisionErrorが代表的な例ですね。
例外が起きると、Pythonはそれ以降の行を実行せずに処理を打ち切ります。そして、どこで何が起きたのかをトレースバックという形で画面に表示します。
トレースバックの読み方に自信がない方は、先にこちらを読んでおくと理解が深まります。
【関連記事】Pythonのトレースバックの読み方
例外は、言ってみればプログラムからのSOSです。try文は、そのSOSを受け取って対応するための窓口だと考えるとイメージしやすいでしょう。
try文の4つのブロックの役割¶
try文は、最大で4つのブロックから組み立てます。まずは、それぞれの役割を表で見比べてみましょう。
| ブロック | 動くタイミング | 主な使い道 |
|---|---|---|
| try | 最初に必ず動く | エラーが起きるかもしれない処理を書く |
| except | tryの中で例外が起きたとき | エラーへの対応を書く |
| else | tryの中で例外が起きなかったとき | 成功したときだけ続けたい処理を書く |
| finally | どんな場合でも最後に動く | ファイルを閉じるなどの後片付けを書く |
必須なのはtryと、exceptかfinallyのどちらか一方です。elseはexceptがあるときにだけ書けます。
4つすべてを使う場面は、実はそれほど多くありません。日常のコードでは、tryとexceptの2つだけで済むことがほとんどです。
基本の流れを実際に動かして確かめる¶
言葉だけではわかりにくいので、4つのブロックをすべて使った関数を用意しました。文字列を整数に変換し、どのブロックが動いたかをprintで表示します。
def to_int(text):
try:
n = int(text)
except ValueError:
print('数字ではありません:', text)
return None
else:
print('変換できました')
return n
finally:
print('後片付け')
print(to_int('42'))
# 変換できました
# 後片付け
# 42
print(to_int('abc'))
# 数字ではありません: abc
# 後片付け
# None
42を渡したときは、try、else、finallyの順に動いています。abcを渡したときは、try、except、finallyの順です。
どちらの場合も、finallyの後片付けが必ず表示されていることに注目してください。exceptやelseの中でreturnしていても、finallyは関数を抜ける直前に必ず実行されます。
ここでひとつ不思議に思いませんか? returnはelseの中に書いてあるのに、後片付けのほうが先に表示されています。
これは、returnで関数を抜けようとした瞬間に、Pythonがいったん待ったをかけてfinallyを実行するためです。戻り値はそのあとで呼び出し元へ届けられます。
exceptの書き方をもう少し詳しく¶
exceptは、ただ書けばよいというものではありません。どの例外を受け止めるのかを、きちんと指定するのが大切です。
例外の種類を指定する¶
except ValueErrorのように書くと、ValueErrorとその仲間だけを受け止めます。それ以外の例外は、そのまま外へ飛んでいきます。
種類を書かずにexceptだけを置くと、あらゆる例外を受け止めてしまいます。Ctrl+Cでプログラムを止めようとしたときのKeyboardInterruptまで捕まえてしまうので、思わぬトラブルのもとになります。
私は10年ほどエンジニアとして開発に関わってきましたが、種類を書かないexceptで原因不明のバグを追いかけたことが何度もあります。エラーが静かに消えてしまうので、どこで失敗したのかの手がかりがまったく残らないのです。
こうしたやりがちな書き方は、別の記事でまとめて紹介しています。
【関連記事】Pythonの例外処理のアンチパターン5選
複数の例外をまとめて受け止める¶
同じ対応で済む例外が複数あるときは、丸カッコでまとめて書けます。asを付けると、起きた例外そのものを変数で受け取れます。
try:
int('x')
except (ValueError, TypeError) as e:
print(type(e).__name__, e)
# ValueError invalid literal for int() with base 10: 'x'
type(e).__name__で、実際に起きた例外の名前を取り出しています。ログに残すときに便利な書き方です。
例外ごとに対応を変えたいときは、exceptを縦に並べます。上から順に調べられ、最初に当てはまった1つだけが実行されます。
そのため、親にあたる広い例外を上に書くと、下の細かい例外には永遠にたどり着きません。細かい例外から先に書くのが鉄則です。
elseはなぜ必要なのか¶
elseの存在を知らない人は意外と多いです。tryの中に全部書けば同じではないか、と感じる方もいるでしょう。
ところが、tryの中に処理を詰め込みすぎると、思わぬ問題が起きます。本当は変換の失敗だけを捕まえたかったのに、そのあとの処理で起きた別のValueErrorまで同じexceptで受け止めてしまうのです。
次の2つの書き方を比べてみましょう。
| 書き方 | tryの中身 | 起きやすい問題 |
|---|---|---|
| elseを使わない | 変換と、その後の処理すべて | 後続の処理のエラーまで入力ミス扱いになる |
| elseを使う | 変換だけ | 捕まえたい例外の範囲がはっきりする |
tryの中は、例外が起きうる最小限の行だけにするのが理想です。成功したあとに続けたい処理はelseへ移すと、どの行のエラーを想定しているのかが読み手にも伝わります。
Python公式チュートリアルでも、elseを使うほうがtryに追加のコードを書くより良いと説明されています。理由はまさに、守るつもりのない処理の例外をうっかり捕まえないためです。
finallyで後片付けを確実に¶
finallyは、例外が起きても起きなくても必ず実行されるブロックです。ファイルを閉じる、データベースとの接続を切る、一時ファイルを消すといった後片付けに向いています。
withで書けるならwithを使う¶
ただし、ファイルを開いて閉じるだけなら、finallyよりwith文のほうがすっきり書けます。withのブロックを抜けるときに、自動でファイルを閉じてくれるからです。
イメージをつかむために、同じ処理を2通りで書いてみました。
# finallyで閉じる書き方
f = open('memo.txt', encoding='utf-8')
try:
print(f.read())
finally:
f.close()
# withで閉じる書き方(こちらがおすすめ)
with open('memo.txt', encoding='utf-8') as f:
print(f.read())
どちらも、読み込みの途中で例外が起きてもファイルは閉じられます。with文の仕組みは、こちらの記事で詳しく解説しています。
【関連記事】Pythonのコンテキストマネージャとは?
finallyの中のreturnは例外を握りつぶす¶
ここからが、今回いちばん伝えたい落とし穴です。finallyの中にreturnを書くと、tryで起きた例外が跡形もなく消えてしまいます。
実際に試してみましょう。0で割るという、確実にエラーになる処理を入れています。
def bad():
try:
1 / 0
finally:
return 'finallyの値'
print(bad())
# finallyの値
ZeroDivisionErrorが起きているはずなのに、何事もなかったかのように文字列が返ってきました。トレースバックも一切表示されません。
これは、finallyの中のreturnが、外へ飛んでいこうとしていた例外を上書きしてしまうためです。breakやcontinueでfinallyを抜けた場合も同じことが起きます。
この挙動はわかりにくく、バグの温床になりやすいと長く指摘されてきました。そこでPEP 765という提案が採択され、Python 3.14からはfinallyを抜けるreturn・break・continueにSyntaxWarningが出るようになっています。
SyntaxWarningは警告なので、プログラム自体は今までどおり動きます。けれど、警告が出たら設計を見直すサインだと受け止めてください。
finallyはあくまで後片付けの場所です。finallyの中には値を返す処理を書かないと覚えておけば、この落とし穴にはまることはありません。
except asの変数はブロックの外で消える¶
もうひとつ、知らないと戸惑う挙動を紹介します。except ValueError as eで受け取った変数eは、exceptブロックを抜けると自動的に削除されます。
実際に、ブロックを抜けたあとでeが残っているか調べてみましょう。
try:
int('x')
except ValueError as e:
pass
print('e' in dir())
# False
ブロックの外でeを使おうとすると、NameErrorになってしまいます。例外の内容をあとで使いたいときは、ブロックの中で別の変数に代入しておきましょう。
この仕組みは、例外オブジェクトが関数の中の変数をいつまでも抱え込み、メモリが解放されなくなるのを防ぐためのものです。Pythonの公式リファレンスにも、この削除の動きが明記されています。
実務でtry文を書くときの心構え¶
ここまでで、try文の文法はひととおり押さえられました。最後に、実務で役立つ考え方をいくつか紹介します。
捕まえる場所を考える¶
例外は、対応できる場所で捕まえるのが基本です。どう対応すればよいかわからない場所で無理に捕まえると、問題を先送りにするだけになってしまいます。
たとえば入力チェックなら、ユーザーに再入力をお願いできる画面側で捕まえるのが自然です。一方、関数の奥深くでは何もせず、例外をそのまま呼び出し元へ伝えたほうがよい場面も多くあります。
独自の例外で意図を伝える¶
アプリが大きくなってくると、ValueErrorのような汎用の例外だけでは意味が伝わりにくくなります。そんなときは、自分で例外クラスを作るという選択肢があります。
【関連記事】Pythonの独自例外(カスタム例外)とは?
私が以前関わった業務システムでは、例外をすべてExceptionで受け止めて、エラーですとだけ画面に出す作りになっていました。問い合わせが来ても原因がわからず、ログを足して再現を待つしかなかった苦い経験があります。
例外の種類をきちんと分けておくことは、未来の自分を助けることにつながります。
まとめ¶
今回は、try・except・else・finallyの4つのブロックの役割と、つまずきやすいポイントを紹介しました。最後に、要点を表で振り返っておきましょう。
| ポイント | 覚えておくこと |
|---|---|
| 動く順番 | try → exceptかelse → finally |
| exceptの書き方 | 例外の種類を必ず指定し、細かいものから並べる |
| elseの役割 | 成功したときだけの処理を書き、tryを短く保つ |
| finallyの役割 | 後片付け専用。ファイル操作ならwithを優先する |
| finallyの中のreturn | 例外を握りつぶすので書かない(3.14から警告) |
| except asの変数 | ブロックを抜けると消える |
例外処理は、エラーを隠すための道具ではありません。エラーが起きたときに、プログラムがどう振る舞うべきかを自分で決めるための道具です。
まずは手元のコードで、tryの中身を必要最小限にするところから始めてみてください。それだけで、エラーの原因を追いかける時間がぐっと短くなるはずです。
ここまでお読みいただきありがとうございました。