Pythonの独自例外の定義

2025-11-01

はじめに

標準的な例外処理は、Pythonに組み込まれた例外クラスを使用して一般的なエラー状況を処理します。しかし、実際のアプリケーション開発では、ビジネスロジックやドメイン固有のエラー条件を表現するために、独自の例外クラスを定義することが非常に重要です。

独自例外を適切に定義することで、プログラムにおけるエラー処理がより明確で効果的になります。まず、状況に応じた意味のあるエラーメッセージを提供できるようになり、問題の原因を理解しやすくなります。また、特定のエラー条件を明確に識別できるため、処理の分岐がわかりやすくなります。さらに、コード全体の可読性と保守性が向上し、開発者間で一貫したエラーハンドリングを実現できる点も大きな利点です。

基本的な独自例外の定義

最もシンプルな独自例外

Pythonで独自例外を定義する最も基本的な方法は、Exceptionクラスを継承することです。次の例は、validate_age()関数で年齢が0未満の場合に例外を発生させ、try-except構文でその例外を捕捉してエラーメッセージを表示します。

class MyCustomError(Exception):
    """独自例外の基本的な例"""
    pass

# 使用例
def validate_age(age):
    if age < 0:
        raise MyCustomError("年齢は0以上である必要があります")
    return age

try:
    validate_age(-5)
except MyCustomError as e:
    print(f"カスタムエラーが発生: {e}")

この例では、MyCustomErrorという独自例外を定義し、年齢が負の値の場合にこの例外を発生させています。

例外メッセージのカスタマイズ

より詳細な情報を伝えるために、例外クラスにメッセージを組み込む方法があります。次のコードは、入力値の検証エラーを扱うための独自例外ValidationErrorを定義しています。

class ValidationError(Exception):
    """バリデーションエラーを表す独自例外"""

    def __init__(self, field, value, message):
        self.field = field
        self.value = value
        self.message = message
        super().__init__(f"フィールド '{field}' の値 '{value}' が無効です: {message}")

# 使用例
def validate_email(email):
    if "@" not in email:
        raise ValidationError("email", email, "メールアドレスの形式が正しくありません")
    return True

try:
    validate_email("invalid-email")
except ValidationError as e:
    print(f"エラー詳細 - フィールド: {e.field}, 値: {e.value}, 理由: {e.message}")

例外にはフィールド名・値・エラーメッセージを保持し、無効な入力内容を詳細に伝えられます。validate_email()関数でメール形式を検証し、不正な場合に例外を発生させ、try-exceptで詳細情報を表示します。

階層化された例外の定義

現実のアプリケーションでは、関連する例外を階層化して定義することが推奨されます。次のコードは、銀行システムにおける独自例外処理の仕組みを示しています。

# ベースとなるカスタム例外
class BankingError(Exception):
    """銀行システムのベース例外"""
    pass

# 特定のエラー種類を表す例外
class InsufficientFundsError(BankingError):
    """残高不足エラー"""

    def __init__(self, current_balance, withdrawal_amount):
        self.current_balance = current_balance
        self.withdrawal_amount = withdrawal_amount
        super().__init__(
            f"残高不足: 現在の残高 {current_balance}円, "
            f"引出額 {withdrawal_amount}円"
        )

class AccountNotFoundError(BankingError):
    """口座不存在エラー"""

    def __init__(self, account_number):
        self.account_number = account_number
        super().__init__(f"口座番号 {account_number} は存在しません")

class InvalidAmountError(BankingError):
    """無効金額エラー"""

    def __init__(self, amount):
        self.amount = amount
        super().__init__(f"無効な金額: {amount}円")

# 使用例
class BankAccount:
    def __init__(self, account_number, balance=0):
        self.account_number = account_number
        self.balance = balance

    def withdraw(self, amount):
        if amount <= 0:
            raise InvalidAmountError(amount)
        if amount > self.balance:
            raise InsufficientFundsError(self.balance, amount)

        self.balance -= amount
        return self.balance

# テスト
account = BankAccount("123456", 10000)

try:
    account.withdraw(15000)
except InsufficientFundsError as e:
    print(f"残高不足エラー: {e}")
except InvalidAmountError as e:
    print(f"無効金額エラー: {e}")
except BankingError as e:
    print(f"その他の銀行エラー: {e}")

BankingErrorを基底クラスとして、InsufficientFundsError(残高不足)、AccountNotFoundError(口座不存在)、InvalidAmountError(無効金額)などの特定エラーを定義しています。BankAccountクラスでは引き出し処理中に条件に応じた例外を発生させ、try-except構文でエラー内容をわかりやすく処理します。

例外階層のメリット

例外を階層化することで、以下のようなメリットがあります。

  1. 細かい制御: 特定の例外だけを捕捉できる
  2. 一括処理: 親例外で関連する例外をまとめて処理できる
  3. 拡張性: 新しい例外を簡単に追加できる
  4. 一貫性: エラーハンドリングが統一される

高度な独自例外パターン

データ検証のための例外

データ検証システムでは、複数の検証エラーをまとめて報告する必要がある場合があります。FieldValidationErrorは個々のフィールドの不正を表し、MultipleValidationErrorsはそれらをまとめて保持します。UserValidatorクラスではユーザー情報のnameemailageを検証し、複数の違反がある場合にまとめて例外を発生させます。これにより、入力データ全体の問題点を一度に把握できます。

class MultipleValidationErrors(Exception):
    """複数の検証エラーをまとめる例外"""

    def __init__(self, errors):
        self.errors = errors
        error_messages = [str(error) for error in errors]
        super().__init__("複数の検証エラー:\n" + "\n".join(error_messages))

class FieldValidationError(Exception):
    """個々のフィールド検証エラー"""

    def __init__(self, field, value, rule):
        self.field = field
        self.value = value
        self.rule = rule
        super().__init__(f"フィールド '{field}' がルール '{rule}' に違反: 値='{value}'")

class UserValidator:
    @staticmethod
    def validate(user_data):
        errors = []

        # 名前の検証
        if "name" not in user_data or not user_data["name"]:
            errors.append(FieldValidationError("name", user_data.get("name"), "必須項目"))

        # メールの検証
        email = user_data.get("email", "")
        if "@" not in email:
            errors.append(FieldValidationError("email", email, "有効なメール形式"))

        # 年齢の検証
        age = user_data.get("age")
        if age is not None and (age < 0 or age > 150):
            errors.append(FieldValidationError("age", age, "0-150の範囲"))

        if errors:
            raise MultipleValidationErrors(errors)

# 使用例
try:
    user_data = {"name": "", "email": "invalid", "age": 200}
    UserValidator.validate(user_data)
except MultipleValidationErrors as e:
    print("検証エラーが発生しました:")
    for error in e.errors:
        print(f"  - {error}")
except FieldValidationError as e:
    print(f"単一の検証エラー: {e}")

コンテキスト情報を含む例外

デバッグを容易にするために、豊富なコンテキスト情報を含む例外を定義できます。次のコードは、発生時の状況(コンテキスト情報)を含む独自例外ContextualErrorを定義した例です。例外にはエラーメッセージのほか、発生時刻・スタックトレース・関連情報(注文IDやユーザーIDなど)を記録できます。process_order()関数では商品がない注文に対してこの例外を発生させ、to_dict()メソッドで辞書形式の詳細情報を取得し、ログ出力やエラーレポートに活用できるようになっています。

import traceback
from datetime import datetime

class ContextualError(Exception):
    """コンテキスト情報を含む例外"""

    def __init__(self, message, context=None):
        self.message = message
        self.context = context or {}
        self.timestamp = datetime.now()
        self.stack_trace = traceback.format_stack()

        # 詳細なメッセージの構築
        detailed_message = f"{message}\n"
        detailed_message += f"発生時刻: {self.timestamp}\n"
        if context:
            detailed_message += "コンテキスト情報:\n"
            for key, value in context.items():
                detailed_message += f"  {key}: {value}\n"

        super().__init__(detailed_message)

    def to_dict(self):
        """例外情報を辞書形式で返す"""
        return {
            "message": self.message,
            "context": self.context,
            "timestamp": self.timestamp.isoformat(),
            "type": self.__class__.__name__
        }

# 使用例
def process_order(order_data):
    try:
        if not order_data.get("items"):
            context = {
                "order_id": order_data.get("id", "不明"),
                "user_id": order_data.get("user_id", "不明"),
                "operation": "order_processing"
            }
            raise ContextualError("注文に商品が含まれていません", context)

        # 処理を続行...
        return "処理成功"

    except ContextualError as e:
        # ロギングなどに利用
        error_info = e.to_dict()
        print("エラー情報:", error_info)
        raise

# テスト
order_data = {"id": "ORD123", "user_id": "USER456", "items": []}
try:
    process_order(order_data)
except ContextualError as e:
    print(f"コンテキスト付きエラー: {e}")

Web APIのエラーレスポンス

REST APIでは、標準化されたエラーレスポンスを返すことが重要です。基底クラスAPIErrorが共通のエラーフォーマット(メッセージ・ステータスコード・エラーコード)を管理し、to_response()でHTTPレスポンス形式の辞書を返します。派生クラスとして、NotFoundError(404)、ValidationError(400)、AuthenticationError(401)などを用意し、用途ごとに適切なレスポ

class APIError(Exception):
    """APIエラーのベースクラス"""

    def __init__(self, message, status_code=500, error_code=None):
        self.message = message
        self.status_code = status_code
        self.error_code = error_code
        super().__init__(self.message)

    def to_response(self):
        """HTTPレスポンス用の辞書を返す"""
        return {
            "error": {
                "code": self.error_code or self.__class__.__name__,
                "message": self.message,
                "status": self.status_code
            }
        }

class NotFoundError(APIError):
    """リソース不存在エラー"""
    def __init__(self, resource_type, resource_id):
        message = f"{resource_type} '{resource_id}' は見つかりません"
        super().__init__(message, status_code=404, error_code="NOT_FOUND")

class ValidationError(APIError):
    """バリデーションエラー"""
    def __init__(self, field_errors):
        self.field_errors = field_errors
        message = "入力データにエラーがあります"
        super().__init__(message, status_code=400, error_code="VALIDATION_ERROR")

    def to_response(self):
        response = super().to_response()
        response["error"]["field_errors"] = self.field_errors
        return response

class AuthenticationError(APIError):
    """認証エラー"""
    def __init__(self, message="認証に失敗しました"):
        super().__init__(message, status_code=401, error_code="UNAUTHORIZED")

# 使用例
def get_user(user_id):
    # ユーザーが存在しない場合
    if user_id not in user_database:
        raise NotFoundError("ユーザー", user_id)

    return user_database[user_id]

def handle_api_request(request):
    try:
        # 何らかの処理
        user = get_user(request.user_id)
        return {"data": user}

    except APIError as e:
        # クライアントに返すエラーレスポンス
        return e.to_response(), e.status_code

ドメイン駆動設計における例外

ドメイン駆動設計(DDD)では、ドメインの概念を例外として表現します。DomainExceptionを基底とし、金額が負の場合のInvalidMoneyAmountError、通貨不一致のCurrencyMismatchError、型不正のInvalidMoneyTypeError、残高不足のInsufficientFundsError(派生的に発生)など、業務上のルールを個別の例外で扱います。これにより、ドメインロジックの意図が明確化され、安全で一貫性のある処理が実現されています。

class DomainException(Exception):
    """ドメイン例外のベースクラス"""
    pass

class Money:
    def __init__(self, amount, currency="JPY"):
        if amount < 0:
            raise InvalidMoneyAmountError(amount)
        self.amount = amount
        self.currency = currency

    def __add__(self, other):
        if self.currency != other.currency:
            raise CurrencyMismatchError(self.currency, other.currency)
        return Money(self.amount + other.amount, self.currency)

class InvalidMoneyAmountError(DomainException):
    def __init__(self, amount):
        self.amount = amount
        super().__init__(f"金額は0以上である必要があります: {amount}")

class CurrencyMismatchError(DomainException):
    def __init__(self, currency1, currency2):
        self.currency1 = currency1
        self.currency2 = currency2
        super().__init__(f"通貨が一致しません: {currency1} vs {currency2}")

class BankAccount:
    def __init__(self, initial_balance=None):
        self.balance = initial_balance or Money(0)

    def deposit(self, amount):
        if not isinstance(amount, Money):
            raise InvalidMoneyTypeError(type(amount))
        self.balance = self.balance + amount

    def withdraw(self, amount):
        if not isinstance(amount, Money):
            raise InvalidMoneyTypeError(type(amount))

        try:
            new_balance = self.balance + Money(-amount.amount)  # 負の値を加算
        except InvalidMoneyAmountError:
            raise InsufficientFundsError(self.balance, amount)

        self.balance = new_balance

class InvalidMoneyTypeError(DomainException):
    def __init__(self, actual_type):
        super().__init__(f"Money型である必要があります: {actual_type}")

# 使用例
try:
    account = BankAccount(Money(1000))
    account.withdraw(Money(1500))  # 残高不足
except DomainException as e:
    print(f"ドメインエラー: {e}")

独自例外のベストプラクティス

意味のある名前の選択

例外クラス名は、その例外が何を表すかを明確に示すべきです。良い例では、InsufficientFundsErrorUserNotFoundErrorのように、何が原因でどのようなエラーなのかが明確にわかる名前を付けています。一方、悪い例のError1MyExceptionは、意味が曖昧で内容が推測できず、コードの可読性や保守性を損ないます。例外名には具体的な状況を反映することが重要です。

# 良い例
class InsufficientFundsError(Exception): pass
class UserNotFoundError(Exception): pass

# 悪い例
class Error1(Exception): pass
class MyException(Exception): pass

適切な継承階層の構築

関連する例外は共通の基底クラスから継承し、エラーハンドリングを簡素化します。

豊富なコンテキスト情報の提供

デバッグを容易にするために、関連する情報を例外に含めます。

ドキュメントの整備

独自例外の使用方法と発生条件をドキュメント化します。

まとめ

独自例外の定義は、Pythonで堅牢でメンテナンス性の高いアプリケーションを構築するための重要なスキルです。基本的な例外定義から、階層化、コンテキスト情報の付加、ドメイン固有の例外まで、段階的に理解を深めることで、実際のプロジェクトで効果的に活用できるようになります。

次の章では、デバッグ技法について学び、発生した例外や問題を効果的に調査・解決する方法を探求します。

演習問題

初級問題

問題1: 基本的な独自例外

ユーザーが入力した数値が正の値であるかを確認するプログラムを作成してください。負の値が入力された場合は、通常のエラーではなく、自分で作成した独自の例外を発生させるようにします。

まず、NegativeNumberError という名前のカスタム例外クラスを作成してください。この例外は、「数値が負である」という特定のエラーを表すために使用します。

次に、check_positive_number() という関数を作成してください。この関数は引数として数値を受け取り、その値が 0 未満の場合は NegativeNumberError を発生させます。エラーメッセージには、たとえば -5 が入力された場合に「数値は正である必要があります: -5」のように、実際の値を含めてください。

数値が正の場合は、そのまま値を返してください。

最後に、try-except 文を使って関数をテストしてください。まず 10 を渡して正常に値が表示されることを確認し、その後 -5 を渡して例外を発生させてください。発生した例外は except で捕捉し、「エラー: 数値は正である必要があります: -5」のように表示してください。

実行結果:

=== 問題1テスト ===
10
エラー: 数値は正である必要があります: -5

問題2: 例外の継承

銀行口座を表すプログラムを作成してください。まず、BankError という名前のカスタム例外クラスを作成してください。次に、BankAccount クラスを作成してください。このクラスは口座残高を管理します。コンストラクタでは初期残高を受け取れるようにしてください。

withdraw() メソッドでは、お金を引き出す処理を行います。引き出し金額が残高より多い場合は、BankError を発生させ、「残高不足です」というメッセージを表示できるようにしてください。正常に引き出せた場合は、引き出した金額と現在の残高を表示してください。

最後に、初期残高1000円の口座を作成し、1500円を引き出して例外が発生することを確認してください。try-except 文を使ってエラーを捕捉し、「エラー: 残高不足です」と表示してください。

問題3: シンプルなバリデーション

ユーザー名をチェックするプログラムを作成してください。まず、UsernameError という名前のカスタム例外クラスを作成してください。

次に、check_username() という関数を作成してください。この関数はユーザー名を受け取り、文字数が3文字未満の場合は UsernameError を発生させ、「ユーザー名は3文字以上必要です」と表示できるようにしてください。3文字以上の場合は、「○○ は有効です」と表示してください。

最後に、"ab""taro" を使ってテストしてください。"ab" の場合は例外が発生し、"taro" の場合は正常に表示されるようにしてください。try-except 文を使ってエラーを捕捉してください。

実行結果:

エラー: ユーザー名は3文字以上必要です
taro は有効です

中級問題

問題4: コンテキスト情報付き例外

ファイルを読み込むプログラムを作成してください。まず、FileError という名前のカスタム例外クラスを作成してください。次に、read_file() という関数を作成してください。この関数はファイル名を受け取り、ファイルを読み込んで内容を表示します。

ファイルが存在しない場合は FileNotFoundError を捕捉し、代わりに FileError を発生させ、「ファイルが見つかりません」というメッセージを表示できるようにしてください。

最後に、sample.txt を読み込むテストを作成してください。ファイルが存在しない場合は、try-except 文を使ってエラーを捕捉し、「エラー: ファイルが見つかりません」と表示してください。

問題5: 複数エラーの収集

ユーザー登録フォームを検証するプログラムを作成してください。まず、FormError というカスタム例外クラスを作成してください。次に、validate_form() 関数を作成してください。この関数では、ユーザー名、メールアドレス、パスワードを受け取り、入力内容を検証します。

  • ユーザー名は3文字以上必要です。
  • メールアドレスには @ が含まれている必要があります。
  • パスワードは8文字以上必要です。

エラーが見つかった場合は、途中で終了せず、すべてのエラーメッセージをリストへ追加してください。そして最後にエラーが1件でもある場合は、FormError を発生させてください。最後に、複数のフォームデータを使ってテストしてください。

1つ目のフォームでは、

  • ユーザー名 "ab"
  • メールアドレス "invalid"
  • パスワード "123"

を使用してください。このフォームでは複数のエラーが発生します。

2つ目のフォームでは、

  • ユーザー名 "taro"
  • メールアドレス "test@example.com"
  • パスワード "password123"

を使用してください。このフォームでは正常に検証成功となります。テストでは try-except 文を使用し、エラー発生時にはすべてのエラーメッセージを表示してください。

問題6: APIエラーの階層

APIエラーを管理するプログラムを作成してください。まず、APIError というカスタム例外クラスを作成してください。このクラスでは、エラーメッセージとステータスコードを保持できるようにしてください。

次に、NotFoundError クラスと BadRequestError クラスを作成してください。どちらも APIError を継承してください。NotFoundError は「ユーザーが見つかりません」というメッセージと、ステータスコード 404 を設定してください。

BadRequestError は「名前は必須です」というメッセージと、ステータスコード 400 を設定してください。その後、get_user() 関数を作成してください。この関数では、ユーザーIDを受け取り、辞書からユーザー名を取得します。

ユーザーID "1""Alice""2""Bob" としてください。存在しないユーザーIDが指定された場合は、NotFoundError を発生させてください。

次に、create_user() 関数を作成してください。この関数では名前を受け取り、空文字の場合は BadRequestError を発生させてください。正常な場合は、「○○ を登録しました」という文字列を返してください。

最後に、以下の3つのテストを実行してください。

  • 1つ目は get_user("1") を実行し、正常に "Alice" を取得してください。
  • 2つ目は get_user("99") を実行し、NotFoundError を発生させてください。
  • 3つ目は create_user("") を実行し、BadRequestError を発生させてください。

テストでは try-except 文を使用し、エラー発生時には、「エラー 404: ユーザーが見つかりません」のように、ステータスコードとエラーメッセージを表示してください。

実行結果:

成功: Alice
エラー 404: ユーザーが見つかりません
エラー 400: 名前は必須です

問題7: リトライ可能例外

リトライ機能を持つプログラムを作成してください。まず、RetryError というカスタム例外クラスを作成してください。次に、unstable_operation() 関数を作成してください。この関数では random.random() を使い、70%の確率で失敗する処理を作成してください。

失敗した場合は RetryError を発生させ、「ネットワークエラー」というメッセージを表示できるようにしてください。成功した場合は "成功" を返してください。

その後、retry() 関数を作成してください。この関数では、受け取った関数を実行し、RetryError が発生した場合は自動でリトライしてください。リトライ回数は最大3回としてください。

エラー発生時には、"失敗: ネットワークエラー"と表示してください。さらに、再実行する前に、"リトライします"と表示してください。リトライの待機時間には time.sleep(1) を使用してください。3回失敗した場合は、最終的に例外を発生させてください。

最後に、try-except 文を使ってテストしてください。成功した場合は "成功" を表示し、最後まで失敗した場合は、"最終的に失敗しました: ネットワークエラー"と表示してください。

問題8: ドメイン例外

商品の在庫管理を行うプログラムを作成してください。まず、InventoryError というカスタム例外クラスを作成してください。次に、OutOfStockError クラスと ProductNotFoundError クラスを作成してください。どちらも InventoryError を継承してください。

その後、Inventory クラスを作成してください。コンストラクタでは、以下の商品データを辞書で管理してください。

  • 商品ID "P001""マウス"、在庫数 5
  • 商品ID "P002""キーボード"、在庫数 0

次に、purchase() メソッドを作成してください。このメソッドでは商品IDと購入数を受け取り、購入処理を行います。指定された商品IDが存在しない場合は、ProductNotFoundError を発生させ、「商品が存在しません」と表示してください。

在庫数より多く購入しようとした場合は、OutOfStockError を発生させ、「在庫が不足しています」と表示してください。正常に購入できた場合は、在庫を減らし、「マウス を 2個 購入しました」のようなメッセージを返してください。

最後に、以下の3つのテストを実行してください。

  • 1つ目は "P001" を2個購入してください。これは正常に成功します。
  • 2つ目は "P001" を10個購入してください。在庫不足エラーになります。
  • 3つ目は "P999" を1個購入してください。商品不存在エラーになります。

テストでは try-except 文を使用し、発生したエラーごとにメッセージを表示してください。

実行結果:

マウス を 2個 購入しました
在庫エラー: 在庫が不足しています
商品エラー: 商品が存在しません

問題9: 例外チェーン

例外チェーンを利用したプログラムを作成してください。まず、DatabaseError というカスタム例外クラスを作成してください。次に、ServiceError というカスタム例外クラスを作成してください。

その後、connect_database() 関数を作成してください。この関数では、データベース接続失敗を模擬するため「データベース接続失敗」というメッセージで DatabaseError を発生させてください。

次に、get_user_data() 関数を作成してください。この関数では connect_database() を呼び出してください。DatabaseError が発生した場合は、その例外を捕捉し、「ユーザー取得失敗」というメッセージを持つ ServiceError を発生させてください。

このとき、"raise ServiceError(...) from e" を使用し、例外チェーンを作成してください。最後に try-except 文を使ってテストしてください。

ServiceError を捕捉したら、「サービスエラー: ユーザー取得失敗」を表示してください。さらに、「e.__cause__」を利用して元の例外を取得し、「原因: データベース接続失敗」を表示してください。

上級問題

問題10: マルチレイヤー例外システム

本問題では、実務でよく使われる「レイヤー別例外設計」を理解するために、アプリケーション全体のエラー処理構造を扱います。システムは「インフラ層」「アプリケーション層」「ドメイン層」の3階層で構成されており、それぞれで異なる例外クラスが定義されています。

まず、ユーザーIDを元にユーザー情報を取得する処理を考えます。ユーザーIDが "123" の場合は正常に処理され、ユーザー情報として「IDが123、名前がUser 123」のようなデータが返されます。一方で、ユーザーIDが "error" の場合は、データベース接続時に問題が発生し、インフラ層で「データベース接続がタイムアウトしました」というエラーが発生します。このエラーはインフラ層例外として扱われ、上位層に伝播します。

さらに、ユーザーIDが空文字("")の場合は、ドメイン層でのバリデーションエラーとなり、「ユーザーIDは必須です」というエラーが発生します。次に、このシステムでは例外が発生した場合でも単純にエラーを表示するのではなく、レイヤーごとに意味のある例外へ変換しながら伝播させる設計になっています。

例えば、インフラ層で発生した「データベース接続がタイムアウトしました」というエラーは、そのまま外部に出すのではなく、アプリケーション層で「ユーザー情報の取得に失敗」という文脈を付加した例外にラップされます。このとき、元の例外情報は失われずに保持される必要があります。

最後に、以下の3つのユーザーIDを使って処理をテストしてください。

1つ目は "123" で、この場合は正常にユーザー情報が取得され、「IDが123のユーザー情報が返る」ことを確認してください。

2つ目は "error" で、この場合はデータベース接続エラーが発生し、最終的にアプリケーション層の例外として「ユーザー情報の取得に失敗」というメッセージが表示されることを確認してください。その際、原因として「データベース接続がタイムアウトしました」が保持されている必要があります。

3つ目は空文字 "" で、この場合はドメイン層でバリデーションエラーが発生し、「ユーザーIDは必須です」というメッセージが表示されることを確認してください。

また、例外が発生した場合は、それがどのレイヤーで発生したのかを必ず識別できるようにしてください。インフラ層・アプリケーション層・ドメイン層のいずれかが明確に分かるように設計する必要があります。

実行結果:

=== 問題10テスト ===

ユーザーID '123' の処理:
✓ 成功: {'id': '123', 'name': 'User 123'}

ユーザーID 'error' の処理:
✗ アプリケーション層エラー: [Application] ユーザー情報の取得に失敗: [Infrastructure] データベース接続がタイムアウトしました
  原因: [Infrastructure] データベース接続がタイムアウトしました

ユーザーID '' の処理:
✗ ドメイン層エラー: [Domain] ユーザーIDは必須です

問題11: 国際化対応例外

本問題では、ユーザー入力のバリデーション処理を多言語対応(国際化対応)で実装します。エラーメッセージは英語・日本語・スペイン語の3言語に対応し、同じエラーでも表示言語が切り替わる設計を理解することが目的です。

ユーザー登録フォームに対してバリデーションを行う処理を作成してください。検証対象のデータは以下の通りです。

ユーザー名(name)は空文字の場合にエラーとし、「フィールドは必須です」という内容になります。
例えば name が空の場合は、英語では「Field 'name' is required」、日本語では「フィールド 'name' は必須です」、スペイン語では「El campo 'name' es obligatorio」と表示される必要があります。

メールアドレス(email)は "invalid" のように @ を含まない場合にエラーとします。この場合は「無効なメール形式」というエラーになります。例えば "invalid" が入力された場合、英語では「Invalid email format: invalid」、日本語では「無効なメール形式: invalid」と表示される必要があります。

パスワード(password)は8文字未満の場合にエラーとします。例えば "short" のような値の場合、英語では「Field 'password' must be at least 8 characters」、日本語では「フィールド 'password' は8文字以上必要です」と表示される必要があります。

エラーは InternationalizedError をベースとしたクラスで管理し、エラーキーとロケール情報をもとに適切なメッセージを生成できるようにしてください。また、同じエラーでも異なるロケールで再表示できる仕組みを持たせてください。

検証処理では複数のエラーが発生する可能性がありますが、今回は簡易的に最初に検出された1件のエラーのみを例外として発生させる仕様とします。

最後に以下のテストデータを使用して動作確認を行ってください。テストデータは以下の通りです。

name は空文字、email は "invalid"、password は "short" としてください。このデータでは必ずエラーが発生します。ロケールは "en""ja""es" の3種類でテストを行い、それぞれの言語で適切なエラーメッセージが表示されることを確認してください。

例えばロケールが "en" の場合は英語のメッセージが表示され、同じエラーでも "ja" に切り替えると日本語メッセージ、"es" に切り替えるとスペイン語メッセージが表示される必要があります。また、発生したエラーは他のロケールに変換して再表示できる仕組みを使い、それぞれの言語で同じエラー内容が正しく表現されることを確認してください。

実行結果:

  [ja] フィールド 'name' は必須です
  [es] El campo 'name' es obligatorio

ロケール: ja
✗ フィールド 'name' は必須です
  [en] Field 'name' is required
  [es] El campo 'name' es obligatorio

ロケール: es
✗ El campo 'name' es obligatorio
  [en] Field 'name' is required
  [ja] フィールド 'name' は必須です

問題12: 監査証跡用例外

本問題では、企業システムにおける「セキュリティ監査付きアクセス制御」を実装します。単なるエラー処理ではなく、異常検知時に監査ログへ自動記録される仕組みを理解することが目的です。あるシステムでは、ユーザーのアクセスを以下の条件で制御しています。

まず、ユーザーID "user123" がロール "admin" で、IPアドレス "192.168.1.100" から "user_data" に対して "read" アクセスする場合は正常アクセスとして許可されます。

一方で、ロールが "guest" のユーザーが "192.168.1.101" から "public_data""read" アクセスする場合は、未承認ロールとしてセキュリティ侵害が発生します。この場合、重要度は "high" として扱われ、「未承認のロールでのアクセス試行」というメッセージが記録されます。

さらに、ユーザーID "user789" がロール "user" の状態で、IPアドレス "192.168.1.102" から "financial_records""write" アクセスを行う場合は、センシティブリソースへの不正書き込みとして扱われ、重要度 "critical" のセキュリティ侵害が発生します。このとき「権限のないセンシティブリソースへの書き込み試行」というメッセージが記録される必要があります。

また、IPアドレスが "10.0.0.100" のように "10.0.0." で始まる内部ネットワークからのアクセスは、たとえ管理者権限であっても監査対象となり、重要度 "medium" のセキュリティ侵害として扱われ、「内部ネットワークからの不正アクセス試行」というメッセージが記録されます。

本システムでは、セキュリティ侵害が発生した場合、単に例外を発生させるだけでなく、以下の情報を必ず含む監査コンテキストを生成します。ユーザーID、IPアドレス、実行アクション、対象リソース、発生時刻に加えて、必要に応じて追加情報(例:試行されたロールやリソース種別)も保持します。

さらに、例外が発生した際には自動的に監査ログへ記録され、重要度に応じて INFO、WARNING、ERROR、CRITICAL のいずれかのレベルでログ出力されます。

最後に以下の4つのアクセスケースで動作確認を行ってください。

1つ目は "user123""admin" 権限で "192.168.1.100" から "user_data""read" アクセスするケースで、この場合は正常にアクセスが許可されることを確認してください。

2つ目は "user456""guest" 権限で "192.168.1.101" から "public_data""read" アクセスするケースで、未承認ロールによるアクセスとしてセキュリティ侵害が発生することを確認してください。

3つ目は "user789""user" 権限で "192.168.1.102" から "financial_records""write" アクセスするケースで、センシティブリソースへの不正書き込みとして重大なセキュリティ侵害が発生することを確認してください。

4つ目は "user999""admin" 権限で "10.0.0.100" から "system_config""read" アクセスするケースで、内部ネットワークからの不正アクセスとして中程度のセキュリティ侵害が発生することを確認してください。

また、セキュリティ侵害が発生した場合は、例外メッセージだけでなく、監査情報を辞書形式で出力し、どのユーザーがどのIPからどのリソースへどの操作を行おうとしたのかが完全に追跡できる状態にしてください。

初級問題

問題1: 基本的な独自例外

class NegativeNumberError(Exception):
    """数値が負の場合の例外"""
    pass

def check_positive_number(number):
    """
    数値が正であることをチェックする関数
    """
    if number < 0:
        raise NegativeNumberError(f"数値は正である必要があります: {number}")
    return number

# テスト
print("=== 問題1テスト ===")
try:
    print(check_positive_number(10))  # 正常
    print(check_positive_number(-5))  # 例外発生
except NegativeNumberError as e:
    print(f"エラー: {e}")

問題2: 例外の継承

class BankError(Exception):
    pass

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise BankError("残高不足です")

        self.balance -= amount
        print(f"{amount}円引き出しました")
        print(f"残高: {self.balance}円")

# テスト
account = BankAccount(1000)

try:
    account.withdraw(1500)
except BankError as e:
    print(f"エラー: {e}")

問題3: シンプルなバリデーション

class UsernameError(Exception):
    pass

def check_username(username):
    if len(username) < 3:
        raise UsernameError("ユーザー名は3文字以上必要です")

    print(f"{username} は有効です")

# テスト
usernames = ["ab", "taro"]

for name in usernames:
    try:
        check_username(name)
    except UsernameError as e:
        print(f"エラー: {e}")

中級問題

問題4: コンテキスト情報付き例外

class FileError(Exception):
    pass

def read_file(filename):
    try:
        with open(filename, "r", encoding="utf-8") as file:
            print(file.read())

    except FileNotFoundError:
        raise FileError("ファイルが見つかりません")

# テスト
try:
    read_file("sample.txt")
except FileError as e:
    print(f"エラー: {e}")

問題5: 複数エラーの収集

class FormError(Exception):
    pass


def validate_form(username, email, password):
    errors = []

    if len(username) < 3:
        errors.append("ユーザー名は3文字以上必要です")

    if "@" not in email:
        errors.append("メールアドレス形式が不正です")

    if len(password) < 8:
        errors.append("パスワードは8文字以上必要です")

    if errors:
        raise FormError(errors)

    return True


# テスト
forms = [
    ("ab", "invalid", "123"),
    ("taro", "test@example.com", "password123")
]

for i, form in enumerate(forms, 1):
    print(f"\nフォーム{i}")

    try:
        validate_form(*form)
        print("検証成功")

    except FormError as e:
        print("入力エラー")

        for error in e.args[0]:
            print(f"- {error}")

問題6: APIエラーの階層

class APIError(Exception):
    def __init__(self, message, status_code):
        self.status_code = status_code
        super().__init__(message)


class NotFoundError(APIError):
    def __init__(self):
        super().__init__("ユーザーが見つかりません", 404)


class BadRequestError(APIError):
    def __init__(self):
        super().__init__("名前は必須です", 400)


def get_user(user_id):
    users = {
        "1": "Alice",
        "2": "Bob"
    }

    if user_id not in users:
        raise NotFoundError()

    return users[user_id]


def create_user(name):
    if not name:
        raise BadRequestError()

    return f"{name} を登録しました"


# テスト
tests = [
    lambda: get_user("1"),
    lambda: get_user("99"),
    lambda: create_user("")
]

for test in tests:
    try:
        result = test()
        print(f"成功: {result}")

    except APIError as e:
        print(f"エラー {e.status_code}: {e}")

問題7: リトライ可能例外

import random
import time


class RetryError(Exception):
    pass


def unstable_operation():
    if random.random() < 0.7:
        raise RetryError("ネットワークエラー")

    return "成功"


def retry(func, retries=3):
    for attempt in range(retries):
        try:
            return func()

        except RetryError as e:
            print(f"失敗: {e}")

            if attempt == retries - 1:
                raise

            print("リトライします")
            time.sleep(1)


# テスト
try:
    result = retry(unstable_operation)

    print(result)

except RetryError as e:
    print(f"最終的に失敗しました: {e}")

問題8: ドメイン例外

class InventoryError(Exception):
    pass


class OutOfStockError(InventoryError):
    pass


class ProductNotFoundError(InventoryError):
    pass


class Inventory:
    def __init__(self):
        self.products = {
            "P001": {"name": "マウス", "stock": 5},
            "P002": {"name": "キーボード", "stock": 0}
        }

    def purchase(self, product_id, quantity):
        if product_id not in self.products:
            raise ProductNotFoundError("商品が存在しません")

        product = self.products[product_id]

        if product["stock"] < quantity:
            raise OutOfStockError("在庫が不足しています")

        product["stock"] -= quantity

        return f"{product['name']} を {quantity}個 購入しました"


# テスト
inventory = Inventory()

tests = [
    ("P001", 2),
    ("P001", 10),
    ("P999", 1)
]

for product_id, quantity in tests:
    try:
        result = inventory.purchase(product_id, quantity)
        print(result)

    except OutOfStockError as e:
        print(f"在庫エラー: {e}")

    except ProductNotFoundError as e:
        print(f"商品エラー: {e}")

問題9: 例外チェーン

class DatabaseError(Exception):
    pass


class ServiceError(Exception):
    pass


def connect_database():
    raise DatabaseError("データベース接続失敗")


def get_user_data():
    try:
        connect_database()

    except DatabaseError as e:
        raise ServiceError("ユーザー取得失敗") from e


# テスト
try:
    get_user_data()

except ServiceError as e:
    print(f"サービスエラー: {e}")
    print(f"原因: {e.__cause__}")

上級問題

問題10: マルチレイヤー例外システム

# ベース例外
class ApplicationException(Exception):
    """アプリケーション全体のベース例外"""

    def __init__(self, message, layer):
        self.message = message
        self.layer = layer
        super().__init__(f"[{layer}] {message}")

# インフラ層例外
class InfrastructureException(ApplicationException):
    """インフラ層例外のベース"""

    def __init__(self, message):
        super().__init__(message, "Infrastructure")

class DatabaseConnectionException(InfrastructureException):
    """データベース接続例外"""
    pass

class ExternalServiceException(InfrastructureException):
    """外部サービス例外"""
    pass

# アプリケーション層例外
class ApplicationServiceException(ApplicationException):
    """アプリケーションサービス層例外のベース"""

    def __init__(self, message):
        super().__init__(message, "Application")

class UserServiceException(ApplicationServiceException):
    """ユーザーサービス例外"""
    pass

class OrderServiceException(ApplicationServiceException):
    """注文サービス例外"""
    pass

# ドメイン層例外
class DomainException(ApplicationException):
    """ドメイン層例外のベース"""

    def __init__(self, message):
        super().__init__(message, "Domain")

class InvalidEntityStateException(DomainException):
    """不正なエンティティ状態例外"""
    pass

class BusinessRuleViolationException(DomainException):
    """ビジネスルール違反例外"""
    pass

# 使用例
class UserRepository:
    """インフラ層: ユーザーレポジトリ"""

    def find_by_id(self, user_id):
        try:
            # データベース操作を模擬
            if user_id == "error":
                raise DatabaseConnectionException("データベース接続がタイムアウトしました")
            return {"id": user_id, "name": f"User {user_id}"}
        except DatabaseConnectionException as e:
            raise

class UserService:
    """アプリケーション層: ユーザーサービス"""

    def __init__(self):
        self.user_repository = UserRepository()

    def get_user_profile(self, user_id):
        try:
            user = self.user_repository.find_by_id(user_id)
            if not user:
                raise UserServiceException(f"ユーザー {user_id} は存在しません")
            return user
        except InfrastructureException as e:
            # インフラ層例外をアプリケーション層例外でラップ
            raise UserServiceException(f"ユーザー情報の取得に失敗: {e}") from e

class User:
    """ドメイン層: ユーザーエンティティ"""

    def __init__(self, user_id, name, email):
        self.user_id = user_id
        self.name = name
        self.email = email
        self._validate()

    def _validate(self):
        if not self.user_id:
            raise InvalidEntityStateException("ユーザーIDは必須です")
        if not self.name:
            raise InvalidEntityStateException("ユーザー名は必須です")
        if "@" not in self.email:
            raise BusinessRuleViolationException("有効なメールアドレス形式ではありません")

# テスト
print("\n=== 問題10テスト ===")
user_service = UserService()

test_cases = [
    "123",     # 正常
    "error",   # インフラ層エラー
    "",        # ドメイン層エラー
]

for user_id in test_cases:
    print(f"\nユーザーID '{user_id}' の処理:")
    try:
        if user_id:
            result = user_service.get_user_profile(user_id)
            print(f"✓ 成功: {result}")
        else:
            user = User("", "", "invalid")  # ドメイン層エラー
    except ApplicationServiceException as e:
        print(f"✗ アプリケーション層エラー: {e}")
        if e.__cause__:
            print(f"  原因: {e.__cause__}")
    except DomainException as e:
        print(f"✗ ドメイン層エラー: {e}")
    except ApplicationException as e:
        print(f"✗ アプリケーションエラー: {e}")

問題11: 国際化対応例外

class InternationalizedError(Exception):
    """国際化対応例外のベースクラス"""

    # エラーメッセージのテンプレート(多言語対応)
    MESSAGE_TEMPLATES = {
        'en': {
            'required': "Field '{field}' is required",
            'invalid_email': "Invalid email format: {value}",
            'min_length': "Field '{field}' must be at least {min_length} characters"
        },
        'ja': {
            'required': "フィールド '{field}' は必須です",
            'invalid_email': "無効なメール形式: {value}",
            'min_length': "フィールド '{field}' は{min_length}文字以上必要です"
        },
        'es': {
            'required': "El campo '{field}' es obligatorio",
            'invalid_email': "Formato de email inválido: {value}",
            'min_length': "El campo '{field}' debe tener al menos {min_length} caracteres"
        }
    }

    def __init__(self, error_key, locale='en', **format_args):
        self.error_key = error_key
        self.locale = locale
        self.format_args = format_args

        # ロケールに応じたメッセージを取得
        message = self._get_localized_message(error_key, locale, format_args)
        super().__init__(message)

    def _get_localized_message(self, error_key, locale, format_args):
        """ロケールに応じたエラーメッセージを取得"""
        if locale not in self.MESSAGE_TEMPLATES:
            locale = 'en'  # フォールバック

        templates = self.MESSAGE_TEMPLATES[locale]
        if error_key not in templates:
            return f"Unknown error: {error_key}"

        return templates[error_key].format(**format_args)

    def with_locale(self, locale):
        """別のロケールで同じ例外を作成"""
        return self.__class__(self.error_key, locale, **self.format_args)

class ValidationErrorI18n(InternationalizedError):
    """国際化対応バリデーションエラー"""
    pass

def validate_user_data_i18n(user_data, locale='en'):
    """
    多言語対応のユーザーデータ検証
    """
    errors = []

    # 名前の検証
    if not user_data.get('name', '').strip():
        errors.append(ValidationErrorI18n('required', locale, field='name'))

    # メールの検証
    email = user_data.get('email', '')
    if email and '@' not in email:
        errors.append(ValidationErrorI18n('invalid_email', locale, value=email))

    # パスワードの検証
    password = user_data.get('password', '')
    if password and len(password) < 8:
        errors.append(ValidationErrorI18n('min_length', locale, field='password', min_length=8))

    if errors:
        # 複数エラーがある場合は最初のエラーのみ表示(実際はまとめて処理する)
        raise errors[0]

    return True

# テスト
print("\n=== 問題11テスト ===")
test_data = {'name': '', 'email': 'invalid', 'password': 'short'}

locales = ['en', 'ja', 'es']
for locale in locales:
    print(f"\nロケール: {locale}")
    try:
        validate_user_data_i18n(test_data, locale)
        print("✓ 検証成功")
    except ValidationErrorI18n as e:
        print(f"✗ {e}")

        # 他のロケールでのメッセージも表示
        for other_locale in [l for l in locales if l != locale]:
            other_error = e.with_locale(other_locale)
            print(f"  [{other_locale}] {other_error}")

問題12: 監査証跡用例外

import logging
import datetime
from dataclasses import dataclass
from typing import Optional

@dataclass
class AuditContext:
    """監査証跡のコンテキスト情報"""
    user_id: str
    ip_address: str
    action: str
    resource: str
    timestamp: datetime.datetime
    additional_info: Optional[dict] = None

class SecurityBreachError(Exception):
    """セキュリティ侵害エラー"""

    def __init__(self, message, severity, audit_context):
        self.message = message
        self.severity = severity  # 'low', 'medium', 'high', 'critical'
        self.audit_context = audit_context
        self.timestamp = datetime.datetime.now()

        # 自動的に監査ログに記録
        self._log_to_audit_trail()

        super().__init__(self._format_message())

    def _format_message(self):
        """フォーマットされたエラーメッセージを作成"""
        ctx = self.audit_context
        return (f"セキュリティ侵害 detected: {self.message} "
                f"[ユーザー: {ctx.user_id}, アクション: {ctx.action}, "
                f"重要度: {self.severity}]")

    def _log_to_audit_trail(self):
        """監査証跡に記録"""
        logger = logging.getLogger('security_audit')

        log_data = {
            'timestamp': self.timestamp.isoformat(),
            'exception_type': self.__class__.__name__,
            'message': self.message,
            'severity': self.severity,
            'user_id': self.audit_context.user_id,
            'ip_address': self.audit_context.ip_address,
            'action': self.audit_context.action,
            'resource': self.audit_context.resource,
            'additional_info': self.audit_context.additional_info
        }

        # 重要度に応じたログレベル
        log_levels = {
            'low': logging.INFO,
            'medium': logging.WARNING,
            'high': logging.ERROR,
            'critical': logging.CRITICAL
        }

        level = log_levels.get(self.severity, logging.ERROR)
        logger.log(level, f"Security breach: {log_data}")

    def to_audit_dict(self):
        """監査用の辞書形式で返す"""
        return {
            'exception': {
                'type': self.__class__.__name__,
                'message': self.message,
                'severity': self.severity,
                'timestamp': self.timestamp.isoformat()
            },
            'context': {
                'user_id': self.audit_context.user_id,
                'ip_address': self.audit_context.ip_address,
                'action': self.audit_context.action,
                'resource': self.audit_context.resource,
                'timestamp': self.audit_context.timestamp.isoformat(),
                'additional_info': self.audit_context.additional_info
            }
        }

# 監査ログの設定
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('security_audit.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)

class SecuritySystem:
    def __init__(self):
        self.authorized_roles = {'admin', 'manager', 'user'}
        self.sensitive_resources = {'user_data', 'financial_records', 'system_config'}

    def check_access(self, user_id, user_role, ip_address, resource, action):
        """
        アクセス権限をチェック
        """
        audit_context = AuditContext(
            user_id=user_id,
            ip_address=ip_address,
            action=action,
            resource=resource,
            timestamp=datetime.datetime.now()
        )

        # ロールの検証
        if user_role not in self.authorized_roles:
            context = AuditContext(
                user_id=user_id,
                ip_address=ip_address,
                action=action,
                resource=resource,
                timestamp=datetime.datetime.now(),
                additional_info={'attempted_role': user_role}
            )
            raise SecurityBreachError(
                "未承認のロールでのアクセス試行",
                "high",
                context
            )

        # センシティブリソースへのアクセス制御
        if (resource in self.sensitive_resources and 
            user_role not in {'admin', 'manager'} and
            action == 'write'):
            context = AuditContext(
                user_id=user_id,
                ip_address=ip_address,
                action=action,
                resource=resource,
                timestamp=datetime.datetime.now(),
                additional_info={'user_role': user_role, 'sensitive_resource': resource}
            )
            raise SecurityBreachError(
                "権限のないセンシティブリソースへの書き込み試行",
                "critical",
                context
            )

        # IPアドレスの検証(簡易版)
        if ip_address.startswith('10.0.0.'):
            context = AuditContext(
                user_id=user_id,
                ip_address=ip_address,
                action=action,
                resource=resource,
                timestamp=datetime.datetime.now(),
                additional_info={'internal_network_access': True}
            )
            raise SecurityBreachError(
                "内部ネットワークからの不正アクセス試行",
                "medium",
                context
            )

        return True

# テスト
print("\n=== 問題12テスト ===")
security_system = SecuritySystem()

test_cases = [
    # (user_id, role, ip, resource, action)
    ("user123", "admin", "192.168.1.100", "user_data", "read"),  # 正常
    ("user456", "guest", "192.168.1.101", "public_data", "read"),  # 未承認ロール
    ("user789", "user", "192.168.1.102", "financial_records", "write"),  # 権限不足
    ("user999", "admin", "10.0.0.100", "system_config", "read"),  # 内部ネットワーク
]

for user_id, role, ip, resource, action in test_cases:
    print(f"\nアクセスチェック: {user_id}({role}) from {ip} -> {resource}.{action}")
    try:
        security_system.check_access(user_id, role, ip, resource, action)
        print("✓ アクセス許可")
    except SecurityBreachError as e:
        print(f"✗ セキュリティ侵害: {e}")
        audit_info = e.to_audit_dict()
        print(f"  監査情報: {audit_info}")