概要
Pythonでは、ValueError や TypeError などの組み込み例外に加えて、アプリケーション固有の例外クラスを定義できます。
例えば、商品登録処理におけるエラーを、次のように表現できます。
class ProductDataError(Exception):
pass
独自例外を使用すると、単なる技術的な失敗ではなく、アプリケーション上の意味を持ったエラーとして扱えます。
ValueError
→ 文字列を整数に変換できない
ProductDataError
→ 商品データとして不正である
独自例外では、主に次の要素を扱います。
-
Exceptionの継承 - 基底例外と具体的例外
-
raiseによる独自例外の送出 -
exceptによる独自例外の捕捉 -
raise ... from ...による例外チェーン - 例外インスタンスへの属性追加
独自例外は、技術的な原因をアプリケーション上の意味へ変換するためのクラスである。
実施条件
- クラスと継承の基本を理解していること
-
try、except、raiseを理解していること - 組み込み例外の基本を理解していること
環境
| ツール | バージョン | 目的 |
|---|---|---|
| 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 には、次のようなプログラム終了に関係する例外も含まれます。
KeyboardInterruptSystemExit
通常のアプリケーションエラーは、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として関連付けられる - 独自例外には、項目名やエラーコードなどの属性を追加できる
- 単純な入力不正では、組み込み例外で十分な場合もある
独自例外は、エラーの種類を増やすためではなく、呼び出し側が適切な判断をできるように、異常状態へ意味を与えるために使用する。