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] 独自例外クラス

0
Posted at

概要

Pythonでは、ValueErrorTypeError などの組み込み例外に加えて、アプリケーション固有の例外クラスを定義できます。

例えば、商品登録処理におけるエラーを、次のように表現できます。

class ProductDataError(Exception):
    pass

独自例外を使用すると、単なる技術的な失敗ではなく、アプリケーション上の意味を持ったエラーとして扱えます。

ValueError
→ 文字列を整数に変換できない

ProductDataError
→ 商品データとして不正である

独自例外では、主に次の要素を扱います。

  1. Exception の継承
  2. 基底例外と具体的例外
  3. raise による独自例外の送出
  4. except による独自例外の捕捉
  5. raise ... from ... による例外チェーン
  6. 例外インスタンスへの属性追加

独自例外は、技術的な原因をアプリケーション上の意味へ変換するためのクラスである。

実施条件

  • クラスと継承の基本を理解していること
  • tryexceptraiseを理解していること
  • 組み込み例外の基本を理解していること

環境

ツール バージョン 目的
Python 3.12 Pythonコードの実行
VS Code 任意 コード編集

独自例外クラスとは

独自例外は、アプリケーション固有の異常状態を表すために、自分で定義する例外クラスです。

通常は、Exception またはそのサブクラスを継承します。

class ProductDataError(Exception):
    pass

使用例です。

raise ProductDataError(
    "商品価格は0以上で指定してください"
)

独自例外も通常のクラスと同様に、インスタンスを生成できます。

error = ProductDataError(
    "商品価格は0以上で指定してください"
)
print(type(error))
# <class '__main__.ProductDataError'>

print(isinstance(error, ProductDataError))
# True

print(isinstance(error, Exception))
# True

例外インスタンスを作っただけでは、例外は発生しません。

error = ProductDataError("価格が不正です")

print(error)
# 価格が不正です

raise することで例外として送出されます。

raise error

独自例外を作る理由

アプリケーション上の意味を表現する

次の処理では、int() によって ValueError が発生します。

int("三千円")

しかし、商品管理アプリケーションの利用者にとって重要なのは、「整数変換に失敗した」という技術的な情報ではなく、「商品価格データが不正である」という業務上の意味です。

class ProductDataError(Exception):
    pass
raise ProductDataError(
    "商品価格を整数として解釈できません"
)

呼び出し側の処理を明確にする

組み込み例外だけで処理すると、どの機能のエラーなのか判別しにくい場合があります。

except ValueError:
    ...

ValueError は、商品価格以外のさまざまな処理でも発生します。

独自例外なら、対象の機能を明確にできます。

except ProductDataError:
    ...

層の責務を分離する

データベースや外部APIなどで発生した低レベルな例外を、サービス層でアプリケーション固有の例外に変換できます。

データベース層
DatabaseError
    ↓

サービス層
ProductSaveError
    ↓

API層
HTTP 500レスポンス

Exceptionを継承する

通常の独自例外は、Exception を継承します。

class ProductError(Exception):
    pass

BaseException を直接継承することは、原則として避けます。

class ProductError(BaseException):
    pass

BaseException には、次のようなプログラム終了に関係する例外も含まれます。

  • KeyboardInterrupt
  • SystemExit

通常のアプリケーションエラーは、Exception の系統に置きます。

基底例外と具体的例外

複数の関連する例外がある場合は、共通の基底例外を定義できます。

class ProductError(Exception):
    """商品処理に関する基底例外。"""


class ProductDataError(ProductError):
    """商品データが不正な場合の例外。"""


class ProductNotFoundError(ProductError):
    """指定された商品が存在しない場合の例外。"""


class ProductSaveError(ProductError):
    """商品データの保存に失敗した場合の例外。"""

継承関係は次のようになります。

Exception
└── ProductError
    ├── ProductDataError
    ├── ProductNotFoundError
    └── ProductSaveError

個別に捕捉する

try:
    process_product()

except ProductDataError as exc:
    print(f"商品データが不正です: {exc}")

except ProductNotFoundError as exc:
    print(f"商品が存在しません: {exc}")

except ProductSaveError as exc:
    print(f"商品の保存に失敗しました: {exc}")

基底例外でまとめて捕捉する

すべての商品関連例外に対して同じ処理をする場合は、ProductError でまとめて捕捉できます。

try:
    process_product()

except ProductError as exc:
    print(f"商品処理に失敗しました: {exc}")

ProductDataError などは、すべて ProductError のサブクラスだからです。

print(
    issubclass(ProductDataError, ProductError)
)
# True

独自例外をraiseする

入力データを検証し、不正な場合に独自例外を発生させます。

class ProductDataError(Exception):
    """商品データが不正な場合の例外。"""


def validate_product(
    name: str,
    price: int,
) -> None:
    if not name:
        raise ProductDataError(
            "商品名を指定してください"
        )

    if price < 0:
        raise ProductDataError(
            "商品価格は0以上で指定してください"
        )
validate_product("", 3000)
# ProductDataError

呼び出し側で捕捉できます。

try:
    validate_product("", 3000)
except ProductDataError as exc:
    print(f"商品登録に失敗しました: {exc}")

出力:

商品登録に失敗しました: 商品名を指定してください

raise ... from ...

raise ... from ... は、ある例外を別の例外へ変換しながら、元の原因を残す構文です。

class ProductDataError(Exception):
    """商品データが不正な場合の例外。"""


def parse_price(raw_price: str) -> int:
    try:
        return int(raw_price)

    except ValueError as exc:
        raise ProductDataError(
            f"価格を整数へ変換できません: "
            f"{raw_price!r}"
        ) from exc
parse_price("三千円")

処理の関係は次のようになります。

int("三千円")
    ↓
ValueError
    ↓ raise ProductDataError(...) from exc
ProductDataError

ValueError が技術的な原因で、ProductDataError がアプリケーション上の意味です。

from excの意味

except ValueError as exc:

ここで、発生した ValueError のインスタンスを exc に入れています。

raise ProductDataError(
    "価格データが不正です"
) from exc

from exc によって、新しい ProductDataError の直接的な原因が exc であることを記録します。

例外インスタンスの __cause__ から確認できます。

try:
    parse_price("三千円")

except ProductDataError as exc:
    print(type(exc))
    print(type(exc.__cause__))

出力:

<class '__main__.ProductDataError'>
<class 'ValueError'>

raise fromを使用しない場合

次のコードでも、例外を変換できます。

try:
    return int(raw_price)

except ValueError:
    raise ProductDataError(
        "価格データが不正です"
    )

この場合、Pythonは処理中に別の例外が発生したことを、暗黙的なコンテキストとして保持します。

一方、raise ... from ... を使うと、直接的な原因を明示できます。

except ValueError as exc:
    raise ProductDataError(
        "価格データが不正です"
    ) from exc

意図的に例外を変換する場合は、raise from を使用した方が因果関係が明確になります。

raise ... from None

元の例外を利用者へ表示したくない場合は、from None を指定できます。

def parse_price(raw_price: str) -> int:
    try:
        return int(raw_price)

    except ValueError:
        raise ProductDataError(
            "商品価格の形式が不正です"
        ) from None

これにより、通常のトレースバックでは元の ValueError が省略されます。

ただし、原因調査に必要な情報まで隠す可能性があるため、使用場所には注意が必要です。

独自例外に属性を持たせる

通常のクラスと同じように、独自例外へ属性を追加できます。

class ProductDataError(Exception):
    """商品データが不正な場合の例外。"""

    def __init__(
        self,
        message: str,
        field_name: str,
        invalid_value: object,
    ):
        super().__init__(message)

        self.field_name = field_name
        self.invalid_value = invalid_value

super().__init__(message) によって、親クラスである Exception にメッセージを渡しています。

def validate_price(price: int) -> None:
    if price < 0:
        raise ProductDataError(
            message="価格は0以上で指定してください",
            field_name="price",
            invalid_value=price,
        )
try:
    validate_price(-100)

except ProductDataError as exc:
    print(exc)
    print(exc.field_name)
    print(exc.invalid_value)

出力:

価格は0以上で指定してください
price
-100

属性を持たせることで、APIレスポンスやログで構造化された情報を使用できます。

error_response = {
    "message": str(exc),
    "field": exc.field_name,
    "invalid_value": exc.invalid_value,
}

エラーコードを持つ独自例外

業務システムでは、エラーコードを持たせることがあります。

class ProductError(Exception):
    """商品処理に関する基底例外。"""

    error_code = "PRODUCT_ERROR"


class ProductDataError(ProductError):
    """商品データが不正な場合の例外。"""

    error_code = "PRODUCT_DATA_ERROR"
try:
    raise ProductDataError(
        "商品価格が不正です"
    )

except ProductError as exc:
    print(exc.error_code)
    print(str(exc))

出力:

PRODUCT_DATA_ERROR
商品価格が不正です

活用例:商品登録処理

class ProductError(Exception):
    """商品処理に関する基底例外。"""


class ProductDataError(ProductError):
    """商品データが不正な場合の例外。"""


class Product:
    def __init__(self, name: str, price: int):
        if not name:
            raise ProductDataError(
                "商品名を指定してください"
            )

        if price < 0:
            raise ProductDataError(
                "商品価格は0以上で指定してください"
            )

        self.name = name
        self.price = price

    def __str__(self) -> str:
        return f"{self.name}: {self.price}"

辞書から商品を生成する関数を定義します。

def create_product(
    data: dict[str, str],
) -> Product:
    try:
        name = data["name"]
        price = int(data["price"])

    except KeyError as exc:
        missing_key = exc.args[0]

        raise ProductDataError(
            f"必須項目がありません: {missing_key}"
        ) from exc

    except ValueError as exc:
        raise ProductDataError(
            "priceは整数で指定してください"
        ) from exc

    return Product(
        name=name,
        price=price,
    )

正常なデータを渡します。

data = {
    "name": "Python入門",
    "price": "3000",
}

try:
    product = create_product(data)

except ProductError as exc:
    print(f"商品登録失敗: {exc}")

else:
    print(f"商品登録成功: {product}")

出力:

商品登録成功: Python入門: 3000円

不正な価格を渡します。

data = {
    "name": "Python入門",
    "price": "三千円",
}

出力:

商品登録失敗: priceは整数で指定してください

内部的には次の処理が行われています。

int("三千円")
    ↓
ValueError
    ↓
except ValueError as exc
    ↓
ProductDataErrorを生成
    ↓
raise ProductDataError(...) from exc
    ↓
呼び出し側のexcept ProductErrorで捕捉

独自例外を作りすぎない

すべてのエラーに独自例外を作る必要はありません。

例えば、単純な関数の引数が不正なだけなら、組み込みの ValueError で十分な場合があります。

def calculate_discount(
    rate: float,
) -> None:
    if not 0 <= rate <= 1:
        raise ValueError(
            "rateは0以上1以下で指定してください"
        )

独自例外が有効なのは、次のような場合です。

  • 呼び出し側で特定機能のエラーを判別したい
  • 複数の低レベル例外を一つの業務例外へまとめたい
  • APIレスポンスのエラー種別を整理したい
  • エラーコードや項目名などの属性を持たせたい
  • ライブラリ利用者へ明確な例外契約を提供したい

まとめ

  • 独自例外は、アプリケーション固有の異常状態を表すクラス
  • 通常は Exception またはそのサブクラスを継承する
  • 独自例外も通常のクラスと同様にインスタンスを生成する
  • インスタンスを作っただけでは例外は発生しない
  • raise によって例外として送出する
  • 共通の基底例外を作ると、関連する例外をまとめて捕捉できる
  • raise ... from ... によって、元の例外との因果関係を明示できる
  • as exc で受け取った例外を from exc として関連付けられる
  • 独自例外には、項目名やエラーコードなどの属性を追加できる
  • 単純な入力不正では、組み込み例外で十分な場合もある

独自例外は、エラーの種類を増やすためではなく、呼び出し側が適切な判断をできるように、異常状態へ意味を与えるために使用する。

参考リンク

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?