0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonの例外処理 — try/exceptの書き方とPHP try/catchとの違い

0
Posted at

はじめに

例外処理はどの言語でも書き方の癖がある。

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("必ず実行される")
  • catchexceptになる
  • 例外クラスの後ろにas eで変数に束縛する
  • finallyはPHPと同じ

基本構造はほぼ同じなので、ここは比較的すんなり入れた。


例外クラスの階層

PHPはExceptionErrorが頂点の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に対応するのがValueErrorRuntimeExceptionに対応するのが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を書かなくて済むのは地味にストレスが減る。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?