はじめに
例外処理はどの言語でも書き方の癖がある。
PHPのtry/catchとPythonのtry/exceptは見た目は似ているが、細かい作法がけっこう違う。特にwithステートメントとコンテキストマネージャの概念はPHPにはないもので、最初は戸惑った。整理しておく。
基本構文
まず見た目の比較から。
<?php
try {
$result = divide(10, 0);
} catch (InvalidArgumentException $e) {
echo "エラー: " . $e->getMessage();
} catch (Exception $e) {
echo "予期しないエラー: " . $e->getMessage();
} finally {
echo "必ず実行される";
}
try:
result = divide(10, 0)
except InvalidArgumentError as e:
print(f"エラー: {e}")
except Exception as e:
print(f"予期しないエラー: {e}")
finally:
print("必ず実行される")
-
catchがexceptになる - 例外クラスの後ろに
as eで変数に束縛する -
finallyはPHPと同じ
基本構造はほぼ同じなので、ここは比較的すんなり入れた。
例外クラスの階層
PHPはExceptionとErrorが頂点の2系統。PythonはBaseExceptionが頂点で、通常はExceptionを継承して使う。
BaseException
├── SystemExit # sys.exit()で発生
├── KeyboardInterrupt # Ctrl+Cで発生
└── Exception # 通常の例外はここ以下
├── ValueError # 値が不正
├── TypeError # 型が不正
├── KeyError # 辞書に存在しないキー
├── IndexError # リストの範囲外アクセス
├── AttributeError # 存在しない属性へのアクセス
├── FileNotFoundError
├── ZeroDivisionError
└── ...
よく遭遇する組み込み例外をまとめた。
# ValueError — 値が不正
int("abc") # ValueError: invalid literal for int()
# TypeError — 型が不正
"hello" + 5 # TypeError: can only concatenate str (not "int") to str
# KeyError — 辞書に存在しないキー
d = {"a": 1}
d["b"] # KeyError: 'b'
# IndexError — リストの範囲外
lst = [1, 2, 3]
lst[10] # IndexError: list index out of range
# AttributeError — 存在しない属性
"hello".foo # AttributeError: 'str' object has no attribute 'foo'
# ZeroDivisionError — ゼロ除算
10 / 0 # ZeroDivisionError: division by zero
PHPのInvalidArgumentExceptionに対応するのがValueError、RuntimeExceptionに対応するのがRuntimeErrorという感覚でだいたい合っている。
複数の例外をまとめてキャッチ
try:
value = int(input("数値を入力: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"入力エラー: {e}")
タプルでまとめて書けるのはPHPにはない書き方。PHPだと同じ処理を書くためにcatchブロックを2つ並べるか、共通の親クラスでキャッチするしかない。
例外を自作する
<?php
class DomainException extends RuntimeException {}
class UserNotFoundException extends DomainException
{
public function __construct(int $userId)
{
parent::__construct("ユーザーID {$userId} は存在しません");
}
}
class DomainError(Exception):
pass
class UserNotFoundError(DomainError):
def __init__(self, user_id: int) -> None:
super().__init__(f"ユーザーID {user_id} は存在しません")
self.user_id = user_id
def find_user(user_id: int) -> dict:
users = {1: {"name": "田中"}, 2: {"name": "鈴木"}}
if user_id not in users:
raise UserNotFoundError(user_id)
return users[user_id]
try:
user = find_user(99)
except UserNotFoundError as e:
print(e) # ユーザーID 99 は存在しません
print(e.user_id) # 99
Pythonの慣習として例外クラスの名前はErrorで終わらせる(Exceptionではなく)。UserNotFoundExceptionではなくUserNotFoundError。最初PHPの命名規則を引きずってExceptionで終わらせていた。
else節 — 例外が発生しなかった場合
Pythonのtry/exceptにはforループと同様にelse節がある。
try:
result = int("42")
except ValueError as e:
print(f"変換失敗: {e}")
else:
# 例外が発生しなかった場合だけ実行される
print(f"変換成功: {result}")
finally:
print("終了")
# 変換成功: 42
# 終了
「成功したときの処理」をtryブロックの中に書かずに済むので、tryの範囲を最小限に絞れる。PHPにはない。
# elseを使わない書き方(tryの範囲が広い)
try:
result = int("42")
print(f"変換成功: {result}") # ここも例外が起きたらcatchされる
except ValueError as e:
print(f"変換失敗: {e}")
例外が起きうる処理とその後の処理を分離できる点で、else節があるほうが設計的にきれい。
raiseと例外の再送出
# 例外を発生させる
raise ValueError("値が不正です")
# 例外をキャッチして別の例外として再送出
try:
result = int("abc")
except ValueError as e:
raise RuntimeError("設定ファイルの形式が不正です") from e
raise ... from eを使うと元の例外を原因として連鎖させられる。
# トレースバックに元の例外が表示される
# RuntimeError: 設定ファイルの形式が不正です
# The above exception was the direct cause of the following exception:
# ValueError: invalid literal for int() with base 10: 'abc'
PHPでもnew RuntimeException("...", 0, $e)で原因例外を渡せるが、Pythonのfrom e構文のほうが明示的で読みやすい。
withステートメントとコンテキストマネージャ
ここがPHPとの一番大きな違い。
PHPでファイルを開くとき、例外が起きても確実にcloseするためにtry/finallyを書く。
<?php
$fp = fopen("file.txt", "r");
try {
$content = fread($fp, filesize("file.txt"));
} finally {
fclose($fp); // 必ずcloseする
}
Pythonはwithステートメントを使う。
with open("file.txt", "r", encoding="utf-8") as f:
content = f.read()
# withブロックを抜けると自動でcloseされる(例外が起きても)
withブロックを抜けるときに自動でリソースの後片付けをしてくれる。これを実現しているのがコンテキストマネージャという仕組み。
複数のリソースを同時に開く
# 入力ファイルを読んで出力ファイルに書く
with open("input.txt", "r", encoding="utf-8") as fin, \
open("output.txt", "w", encoding="utf-8") as fout:
for line in fin:
fout.write(line.upper())
PHPだと2つのtry/finallyをネストする必要がある場面が、withでフラットに書ける。
withは自作できる
__enter__と__exit__を実装すれば自分でコンテキストマネージャを作れる。
class Timer:
def __enter__(self):
import time
self._start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
elapsed = time.perf_counter() - self._start
print(f"経過時間: {elapsed:.4f}秒")
return False # Falseを返すと例外を握りつぶさない
with Timer():
total = sum(range(10_000_000))
# 経過時間: 0.2156秒
contextlib.contextmanagerを使うとジェネレーターで書けてさらにシンプル。
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"経過時間: {elapsed:.4f}秒")
with timer():
total = sum(range(10_000_000))
# 経過時間: 0.2156秒
よくあるアンチパターン
例外を握りつぶす
# 悪い例
try:
result = some_function()
except Exception:
pass # 何も知らせない
PHPでも同じだが、except Exception: passは原因不明のバグを生む。せめてログを残す。
# まだマシな書き方
import logging
try:
result = some_function()
except Exception as e:
logging.exception("予期しないエラーが発生しました")
raise # 再送出して呼び出し元に知らせる
広すぎるtryブロック
# 悪い例 — 何の例外が起きたかわかりにくい
try:
data = fetch_data(url)
parsed = parse(data)
saved = save(parsed)
except Exception as e:
print(f"エラー: {e}")
# 良い例 — 処理ごとに例外の種類を分ける
try:
data = fetch_data(url)
except ConnectionError as e:
print(f"通信エラー: {e}")
return
try:
parsed = parse(data)
except ValueError as e:
print(f"パースエラー: {e}")
return
save(parsed)
まとめ
| 項目 | PHP | Python |
|---|---|---|
| 基本構文 | try/catch/finally |
try/except/finally |
| 複数例外のまとめ | catchを複数並べる | タプルで一括(A, B)
|
| 例外なし時の処理 | なし |
else節 |
| 例外の連鎖 | new Exception("", 0, $prev) |
raise X from e |
| リソース管理 | try/finally |
withステートメント |
| 例外名の慣習 | 〜Exception |
〜Error |
withステートメントはPHPにはない概念だが、慣れると手放せなくなる。ファイル・DB接続・ロックなど「使い終わったら閉じる」処理を書くときに毎回try/finallyを書かなくて済むのは地味にストレスが減る。