Python | テスト・設計・品質:例外階層

Python Python
スポンサーリンク

例外階層って何?一言でいうと「エラーを“種類ごとに整理する”ための仕組み」

例外階層(Exception Hierarchy)は、
「エラーを種類ごとに分類して、親子関係で整理する」ための設計です。

Python にはすでに標準の例外階層がありますが、
自分のアプリでも 「どんなエラーが起きうるか」 を整理して階層を作ると、

どこで何を捕まえるべきか
どのエラーは握りつぶしてよくて
どのエラーは上に伝えるべきか

が一気に分かりやすくなります。

例外階層は、テスト・設計・品質のすべてに効く“基礎体力”のようなものです。


Python標準の例外階層をざっくり理解する

Pythonの例外は「木構造」になっている

Python の例外は、こんな感じの階層になっています。

BaseException
 ├── Exception
 │    ├── ValueError
 │    ├── TypeError
 │    ├── KeyError
 │    ├── RuntimeError
 │    └── ...
 └── SystemExit / KeyboardInterrupt など

重要なのは、

except Exception: は「ほとんどのエラー」を捕まえる
except ValueError: は「値が不正なときだけ」を捕まえる

というように、親を捕まえれば子も捕まるという仕組みです。

この仕組みを、自分のアプリでも活用するのが「例外階層の設計」です。


例外階層を自作するメリットを体感する

悪い例:例外がバラバラで扱いにくい

例えば、ユーザー登録処理でこんなコードがあるとします。

def register_user(name: str, email: str) -> None:
    if not name:
        raise ValueError("name is empty")
    if "@" not in email:
        raise ValueError("invalid email")
    if email_exists(email):
        raise RuntimeError("email already exists")
Python

これだと、

どれが「入力エラー」で
どれが「業務ルールのエラー」で
どれが「システムのエラー」なのか

が分かりません。

呼び出し側も困ります。

try:
    register_user("Taro", "taro@example.com")
except Exception:
    print("なんかエラー")
Python

「なんかエラー」では困りますよね。

良い例:例外階層を作って整理する

まずは「アプリ専用の基底例外」を作ります。

class AppError(Exception):
    """アプリ全体の基底例外"""
Python

次に、種類ごとに例外を分けます。

class ValidationError(AppError):
    """入力値が不正"""

class DomainError(AppError):
    """ビジネスルール違反"""

class DuplicateEmailError(DomainError):
    """メールアドレスが既に存在する"""
Python

これを使って書き直すとこうなります。

def register_user(name: str, email: str) -> None:
    if not name:
        raise ValidationError("name is empty")
    if "@" not in email:
        raise ValidationError("invalid email")
    if email_exists(email):
        raise DuplicateEmailError("email already exists")
Python

呼び出し側は、種類ごとに正しく扱えます。

try:
    register_user("Taro", "taro@example.com")
except ValidationError as e:
    print("入力エラー:", e)
except DomainError as e:
    print("業務ルールエラー:", e)
Python

これが例外階層の威力です。


例外階層の設計で特に大事なポイントを深掘りする

「基底例外」を必ず作る

アプリ全体の例外の親を作るのは、ほぼ必須です。

class AppError(Exception):
    pass
Python

これがあると、

「アプリ内のエラーだけをまとめて捕まえる」
「外部ライブラリのエラーは捕まえない」

ということができます。

try:
    do_something()
except AppError:
    print("アプリ内のエラーだけ処理する")
Python

これがないと、ValueErrorTypeError などの標準例外まで巻き込んでしまい、
バグを隠す危険があります。

「分類」を意識して階層を作る

例外階層は、次のような分類で作ると整理しやすいです。

入力の問題(ValidationError)
ビジネスルールの問題(DomainError)
外部サービスの問題(ExternalServiceError)
永続化の問題(RepositoryError)

例えば、注文処理ならこうなります。

class OrderError(AppError):
    pass

class OrderValidationError(OrderError):
    pass

class PaymentError(OrderError):
    pass

class InventoryError(OrderError):
    pass
Python

これで、

「注文に関するエラーだけまとめて捕まえる」
「支払いエラーだけ個別に扱う」

といった柔軟な制御ができます。

「例外メッセージ」も設計の一部

例外メッセージは、ログに残る大事な情報です。

悪い例:

raise ValidationError("invalid")
Python

良い例:

raise ValidationError(f"invalid email: {email}")
Python

未来の自分がログを見たときに、
「何がどうダメだったのか」が分かるように書くのがポイントです。


例外階層とテストの関係

テストは「どの例外が投げられるか」を確認できる

例外階層があると、テストも書きやすくなります。

import pytest

def test_duplicate_email():
    with pytest.raises(DuplicateEmailError):
        register_user("Taro", "taro@example.com")
Python

「このケースではこの例外が投げられるべき」という仕様が明確になります。

「広く捕まえる」「狭く捕まえる」を選べる

例えば、ユースケース層では「アプリ内のエラーだけ」捕まえたい。

try:
    register_user(...)
except AppError as e:
    logger.error("user registration failed: %s", e)
Python

一方、ドメイン層では「標準例外はバグなので捕まえない」。

def validate_email(email: str) -> None:
    if "@" not in email:
        raise ValidationError("invalid email")
    # TypeError などはバグなので捕まえない
Python

例外階層があると、こうした「層ごとの方針」が明確にできます。


例外階層と品質の関係

バグと仕様エラーを区別できる

例外階層がないと、すべてのエラーが「ただの例外」になります。

例外階層があると、

ValidationError → ユーザーの入力ミス
DomainError → 業務ルール違反
RepositoryError → DBの問題
AppError → アプリ内の仕様エラー
TypeError / ValueError → バグ

というように、「これはバグか?仕様か?」 が明確になります。

これは品質に直結します。

ログの読みやすさが段違いになる

例外階層があると、ログも整理されます。

ERROR DomainError: email already exists
ERROR ValidationError: invalid email
ERROR RepositoryError: DB connection failed

種類が分かれているので、
「どの層で問題が起きているか」が一目で分かります。


初心者が例外階層を作るときのおすすめステップ

まずは「アプリ基底例外」を作る

class AppError(Exception):
    pass
Python

これだけで、例外設計の第一歩です。

次に「入力」「ドメイン」「外部」の3つに分ける

class ValidationError(AppError):
    pass

class DomainError(AppError):
    pass

class ExternalServiceError(AppError):
    pass
Python

これだけでも、かなり整理されます。

必要になったら「細かい例外」を追加する

例えば、メール重複だけ特別扱いしたいなら、

class DuplicateEmailError(DomainError):
    pass
Python

というように、必要になったときに追加していけばOKです。


まとめ(例外階層は「エラーを種類ごとに整理して扱いやすくする設計」)

初心者向けにまとめると、例外階層はこういうものです。

例外階層は、エラーを種類ごとに分類し、親子関係で整理することで、「どこで何を捕まえるか」「どれがバグでどれが仕様か」を明確にする設計。
アプリ基底例外 → 入力エラー / ドメインエラー / 外部エラー → 個別のエラー、という階層を作ると、テストしやすく、ログも読みやすく、品質が安定する。
最初は「AppError」「ValidationError」「DomainError」だけでも十分で、必要に応じて細かい例外を追加していくと、自然と扱いやすい例外体系が育っていく。

タイトルとURLをコピーしました