モダンPython基礎 #6 エラーと例外処理
#5 関数 — 引数パターン で関数の表現力を押さえました。今回は その関数が失敗するときどう扱うか — 例外処理です。
Python は 例外 (exception) でエラーを表す言語です。他の言語のようにエラーコードを返すスタイルではなく、「異常事態は投げて、受け取る人がキャッチする」が基本です。EAFP (Easier to Ask for Forgiveness than Permission) という表現でよく呼ばれます。
最も簡単な try / except #
def divide(a: int, b: int) -> float:
try:
return a / b
except ZeroDivisionError:
return float("inf")
print(divide(10, 2)) # 5.0
print(divide(10, 0)) # inf文法:
try:ブロックの中で例外が発生しうるコードを実行except 例外型:でキャッチ- キャッチされなかった例外は上に伝播 (caller に上がる)
LBYL vs EAFP #
他の言語は通常「使う前に確認」(LBYL — Look Before You Leap) スタイルです。
if b != 0:
result = a / b
else:
result = float("inf")Python は EAFP の方が慣用的です。
try:
result = a / b
except ZeroDivisionError:
result = float("inf")理由: Python は動的型付けなので 「条件を正確に事前確認する」 のが難しい場面が多く、例外処理のコストも (例外が頻発する場合でなければ) 低いからです。
複数の例外をキャッチ #
def parse_int(value: str) -> int | None:
try:
return int(value)
except (ValueError, TypeError):
return None
print(parse_int("42")) # 42
print(parse_int("abc")) # None
print(parse_int(None)) # None(例外1, 例外2) のように tuple でまとめてキャッチします。
別々に処理したいなら分離 #
try:
do_work()
except ValueError as e:
print(f"不正な値: {e}")
except KeyError as e:
print(f"キーがない: {e}")
except Exception as e:
print(f"その他: {e}")
raise # 再度投げるas e で例外オブジェクトを受け取ります。メッセージ (str(e)) や原因 (e.__cause__) のような情報を取り出せます。
else と finally
#
try には二つの追加節があります。
def read_file(path: str) -> str:
f = None
try:
f = open(path)
data = f.read()
except FileNotFoundError:
return ""
else:
# 例外なしで try が終わったときだけ実行
print(f"読み取り成功: {len(data)} bytes")
return data
finally:
# 例外発生の有無に関係なく常に実行
if f is not None:
f.close()else—tryが例外なく終わったときだけ。「成功時にだけやること」がある場面finally— 例外の有無と関係なく 常に。リソース整理 (ファイル close、ロック解放) に使う
上のコードは実は with 文でもっとすっきり書けます。
def read_file(path: str) -> str:
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
return ""with が finally の close 責任を引き受けます。次のシリーズ (Python 中級) でコンテキストマネージャとして詳しく扱うトピックです。
raise — 自分で投げる
#
def withdraw(balance: int, amount: int) -> int:
if amount <= 0:
raise ValueError("出金額は正の数でなければなりません")
if amount > balance:
raise ValueError(f"残高不足: {balance} < {amount}")
return balance - amountraise の後に例外インスタンス (またはクラス) を書きます。raise ValueError (クラスのみ) も可能ですが、メッセージも一緒に書く raise ValueError("...") が一般的です。
再投げ #
except の中で処理はしたが呼び出し元にも知らせたいとき。
try:
do_work()
except KeyError:
log("キーなし発生")
raise # 引数なしの raise — キャッチした例外をそのまま再度投げるraise だけ書くと 現在キャッチした例外をそのまま 上に投げます。trace が保存されます。
raise ... from ... — 原因の連鎖
#
元の例外を別の例外で包みたいとき。
def load_config(path: str) -> dict:
try:
with open(path) as f:
return parse(f.read())
except (OSError, ValueError) as e:
raise ConfigError(f"設定ロード失敗: {path}") from efrom e があると traceback に 「この例外の直接の原因は e」 という連結が表示されます。デバッグ時に本当に役立ちます。
例外階層 #
Python の例外はクラス階層です。親をキャッチすれば子もすべてキャッチされます。
BaseException
├─ SystemExit ← sys.exit()
├─ KeyboardInterrupt ← Ctrl+C
└─ Exception ← 通常はここ以下のみキャッチ
├─ ValueError
│ └─ UnicodeError
├─ TypeError
├─ LookupError
│ ├─ KeyError
│ └─ IndexError
├─ ArithmeticError
│ └─ ZeroDivisionError
├─ OSError
│ └─ FileNotFoundError
└─ ...重要なルール: 一般的なコードでは except Exception までだけキャッチしてください。except BaseException や bare except: は KeyboardInterrupt のようなシステムシグナルまで飲み込んで、Ctrl+C が効かない事故を作ります。
try:
risky()
except: # bare except — KeyboardInterrupt もキャッチされる
pass
# あるいは
except BaseException: # 同じ問題
passtry:
risky()
except Exception as e:
log(e)ユーザー定義例外 #
ライブラリ / モジュールを作るときは、自分の例外クラス を定義しておくのが良いです。呼び出し元が何をキャッチすべきかを明示できます。
class AppError(Exception):
"""このアプリのすべての例外のベース。"""
class ConfigError(AppError):
"""設定関連エラー。"""
class DatabaseError(AppError):
"""DB 関連エラー。"""
class RowNotFoundError(DatabaseError):
"""必要な行がありません。"""
def __init__(self, table: str, key: str):
super().__init__(f"{table} で {key} が見つからない")
self.table = table
self.key = key呼び出し側は:
try:
user = repo.get_user("u1")
except RowNotFoundError as e:
return Response(status=404)
except DatabaseError as e:
log_critical(e)
return Response(status=500)それぞれの場面で 必要な分だけ キャッチします。
finally の罠 — return の優先順位
#
finally で return すると try の return を上書きします。ほぼアンチパターンです。
def f() -> int:
try:
return 1
finally:
return 2 # こちらが勝つ
print(f()) # 2finally は 副作用の整理 だけにとどめ、return や raise はできるだけしない のが良いです。
Exception Group — except* (3.11+)
#
複数の作業が 同時に 失敗しうる場面 (asyncio の TaskGroup、並列作業) で、複数の例外を一度にまとめて投げられる新しいメカニズムが入りました。これが ExceptionGroup で、それをキャッチする文法が except* です。
errors = ExceptionGroup(
"複数の作業が失敗",
[ValueError("a"), TypeError("b"), ValueError("c")],
)
raise errorsexcept* はグループの中から 該当タイプの例外だけ拾い 、残りは再度投げます。
try:
raise ExceptionGroup(
"複数の失敗",
[ValueError("a"), TypeError("b"), ValueError("c")],
)
except* ValueError as eg:
print("値エラーたち:", [str(e) for e in eg.exceptions])
except* TypeError as eg:
print("型エラーたち:", [str(e) for e in eg.exceptions])いつ出会うのか? asyncio.TaskGroup で複数の task を同時に回すとき、そのうち二つ以上が失敗すると自動で ExceptionGroup が作られます。非同期は次のシリーズで扱いますが、「複数の例外が一度に来うる場面がある」という事実だけ覚えておいてください。
assert — 開発中の仮説検査
#
def calc_average(items: list[int]) -> float:
assert len(items) > 0, "空リストでは平均が出せない"
return sum(items) / len(items)assert 条件, メッセージ — 条件が False なら AssertionError が起きます。
注意: python -O (optimization) モードではすべての assert が 削除されます。 したがって:
- ✅ 開発中の仮説検査 (「ここでは絶対 None ではないはず」のようなもの)
- ❌ ユーザー入力の検証 — これは常に通常の
if+raiseでやるべき
def serve(user: User | None):
# ✅ システム仮説: 認証ミドルウェアが None を防いだはず
assert user is not None, "auth middleware 抜け?"
# ❌ ユーザー入力は assert で防いではダメ
# assert age >= 0, "負の年齢不可" ← -O で消える
if age < 0:
raise ValueError("年齢は 0 以上")良い例外処理の感覚 #
最後にコードレビューでよく登場するパターンを整理します。
1) 広くキャッチしすぎない #
try:
process(item)
except Exception:
pass # すべての例外を無視 — バグを隠すtry:
process(item)
except ValueError as e:
log(f"不正な項目: {e}")2) try ブロックは短く #
try:
a = compute()
b = transform(a)
c = validate(b)
d = save(c)
return d
except SomeError:
...SomeError がどの行で発生したか分かりません。怪しい一行だけを囲んでください。
3) pass だけの except は疑う
#
空の except: pass はほぼ常にバグの始まりです。本当に無視してよい場面なら 理由をコメントで残すか、少なくともログは残してください。
4) 通常のフローに例外を使わない #
例外は 異常事態 用です。通常の分岐まで try/except で書くと、コードを読む人が罠にはまります (ただし Python は EAFP が慣用的なので、ある場面ではそれが正常 — int("abc") のような変換失敗処理など)。
まとめ #
今回押さえたもの:
- EAFP — Python では試してキャッチする方が自然な場面が多い
try/except/else/finallyの使い分け- リソース整理は
finallyよりもwithの方が普通きれい raiseとraise ... from ...(原因の連結)except Exceptionまで —BaseExceptionや bareexcept:はダメ- ユーザー定義例外はベースクラスを置いて階層で作る
assertは仮説検査用、入力検証は絶対if + raiseExceptionGroup+except*(3.11+) — 複数例外の同時処理 (asyncio TaskGroup など)
次回 (#7 モジュール / パッケージと pyproject.toml) では 複数のファイルにコードを分ける方法 — import システム、モジュールとパッケージの違い、__init__.py、__main__、そして pyproject.toml まで一箇所に整理します。