PythonにはValueErrorやKeyErrorといった組み込みの例外がありますが、自分のプログラム特有のエラーには、独自の例外(カスタム例外)を作るのが効果的です。たとえば「在庫が足りない」「APIの認証に失敗した」といったエラーにOutOfStockError・AuthErrorのような意味のある名前を付けると、何が起きたのかがコードから一目で分かり、エラーの種類ごとに処理を分けやすくなります。
カスタム例外は、Exceptionを継承したクラスとして作ります。ポイントは、例外には親子関係(階層)があり、親の例外で受けると子の例外もまとめて捕捉できることです。この記事では、実機のPythonで確認しながら、カスタム例外の作り方を整理します。
- カスタム例外は
class MyError(Exception): passで作ります(Exceptionを継承)。 - 発生は
raise MyError("メッセージ")、捕捉はexcept MyError as e:です。 - 共通の親クラスを作ると、親で受けて子の例外もまとめて捕捉できます。
__init__を定義すると、ステータスコードなど独自の属性を持たせられます。raise 新しい例外 from 元の例外で、元の例外の情報を残せます。- 意味のある例外名で、エラーの原因と処理の分岐が明確になります。
例外処理の基本は例外処理(try/except)、例外クラスの土台となるクラス(class)、__init__を持つ関数(def)もあわせて参考になります。
カスタム例外とは(なぜ作るか)
組み込みのValueErrorなどでもエラーは表せますが、プログラム特有の状況には専用の例外名を付けるほうが分かりやすくなります。raise ValueError("在庫不足")よりもraise OutOfStockError("在庫不足")のほうが、エラーの種類がはっきりし、except OutOfStockError:でその状況だけを狙って処理できます。ライブラリやフレームワークも、独自の例外を定義しているものがほとんどです。
最小のカスタム例外(Exception継承)
カスタム例外は、Exceptionを継承したクラスを作るだけです。中身はpassでかまいません。raiseで発生させ、exceptで捕捉します。
# Exception を継承するだけ(中身は pass でOK)
class ValidationError(Exception):
pass
# 発生させる
try:
raise ValidationError("入力が不正です")
except ValidationError as e:
print(e) # 入力が不正です
print(type(e).__name__) # ValidationError
実機でも、ValidationErrorをraiseしてexcept ValidationError as e:で捕捉でき、str(e)でメッセージ、type(e).__name__で例外名が取得できました。class ValidationError(Exception): passと書くだけで、独自の例外が完成します。メッセージはraise ValidationError("...")のように渡すと、そのままstr(e)で取り出せます。必ずException(またはそのサブクラス)を継承してください。BaseExceptionを直接継承するのは推奨されません。
例外の階層(親クラスでまとめて捕捉)
カスタム例外の真価は階層(親子関係)にあります。共通の親例外を作り、個別の例外をその子として定義すると、親の例外でexceptすれば、子の例外もまとめて捕捉できます。
# 共通の親例外
class AppError(Exception):
pass
# 個別の例外(AppError を継承)
class NotFoundError(AppError):
pass
class AuthError(AppError):
pass
# 親の AppError で受けると、子もまとめて捕捉できる
try:
raise NotFoundError("ユーザーが見つかりません")
except AppError as e: # NotFoundError も AuthError もここで受かる
print("アプリのエラー:", e)
実機で確認したところ、AppErrorを継承したNotFoundErrorをraiseしても、親のexcept AppError as e:で正しく捕捉できました(isinstance(e, AppError)もTrue)。これは、exceptは指定した例外クラスと、そのサブクラス(子)をすべて受けるためです。この性質を使うと、アプリ全体の例外に共通の親AppErrorを持たせておき、except AppError:で「自分のアプリが投げたエラー」だけをまとめて処理する、といった設計ができます。逆に、個別に対応したいときはexcept NotFoundError:のように子の例外を直接指定します。「まとめて処理」と「個別に処理」を、階層で使い分けられるのがカスタム例外の大きな利点です。なお、組み込みのexcept Exception:はすべての例外を受けるため、カスタム例外も当然そこで捕捉されます。
独自の属性を持たせる
例外にメッセージ以外の情報(ステータスコード、エラーコード、対象の値など)を持たせたいときは、__init__を定義します。super().__init__()でメッセージを親に渡しつつ、独自の属性を追加します。
class APIError(Exception):
def __init__(self, message, status_code):
super().__init__(message) # メッセージは親に渡す
self.status_code = status_code # 独自の属性
try:
raise APIError("リクエスト失敗", 404)
except APIError as e:
print(str(e)) # リクエスト失敗
print(e.status_code) # 404(独自の属性を取り出せる)
実機でも、APIError("リクエスト失敗", 404)からstr(e)でメッセージ、e.status_codeで404という独自の属性を取り出せました。super().__init__(message)でメッセージを親(Exception)に渡すことで、str(e)でメッセージが取れるようになります。そのうえでself.status_code = status_codeのように属性を追加すれば、例外を捕捉した側でその情報を使って処理を分けられます。HTTPのステータスコードや、どの値でエラーになったかなど、対処に必要な情報を例外に持たせると便利です。
raise from で元の例外を残す
ある例外を捕まえて別の例外に変換して投げ直すとき、raise 新しい例外 from 元の例外とすると、元の例外の情報が失われず、原因をたどれます。
class AppError(Exception):
pass
try:
try:
value = int("abc") # ValueError が起きる
except ValueError as orig:
# 元の例外(orig)を残しつつ、自前の例外に変換
raise AppError("数値変換に失敗しました") from orig
except AppError as e:
print(e) # 数値変換に失敗しました
print(type(e.__cause__)) # <class 'ValueError'>(元の例外)
実機でも、raise AppError(...) from origとすると、e.__cause__に元のValueErrorが保持されていることを確認できました。fromを使わずにただraise AppError(...)とすると、元の例外の情報が分かりにくくなりますが、from 元の例外を付けると、エラーメッセージに「上記の例外が直接の原因です」と表示され、本当の原因(ValueError)までさかのぼれます。ライブラリの内部例外を、自分のアプリの分かりやすい例外に変換しつつ、デバッグのために原因を残す、という場面で役立ちます。
主なポイントまとめ
カスタム例外の要点をまとめます。
| 項目 | 書き方 |
|---|---|
| 定義 | class MyError(Exception): pass |
| 発生 | raise MyError("メッセージ") |
| 捕捉 | except MyError as e: |
| 階層 | 親でexceptすると子もまとめて捕捉 |
| 独自属性 | __init__+super().__init__(message) |
| 原因を残す | raise 新例外 from 元例外 |
よくある失敗
Exceptionを継承しない
カスタム例外はExceptionを継承します。BaseExceptionの直接継承は避けます。
独自属性でsuper().__init__を呼ばない
str(e)でメッセージが取れなくなります。super().__init__(message)を呼びます。
親例外を子より先にexceptに書く
親が先だと子のexceptに届きません。子(具体的)を先、親(広い)を後に書きます。
例外を握りつぶす
except: passで何もしないと原因が分かりません。最低限ログを残します。
raise fromを使わず原因を失う
変換時はfrom 元の例外を付けて、原因をたどれるようにします。
よくある質問
class MyError(Exception): passのように、Exceptionを継承したクラスを作るだけです。raise MyError("メッセージ")で発生させ、except MyError as e:で捕捉します。プログラム特有のエラーに意味のある名前を付けられ、エラーの種類ごとに処理を分けやすくなります。except 独自例外:でその状況だけを狙って処理できるためです。ValueErrorのような汎用的な例外だと、どんな原因のエラーかが区別しにくくなります。AppError)を作り、個別の例外をその子として定義します。except AppError:とすれば、子の例外もまとめて捕捉できます。exceptは指定したクラスとそのサブクラスをすべて受けるためです。__init__を定義し、super().__init__(message)でメッセージを親に渡したうえで、self.status_code = status_codeのように属性を追加します。捕捉した側でe.status_codeとして取り出し、処理を分けられます。raise AppError(...) from 元の例外とすると、__cause__に元の例外が保持され、エラー表示でも本当の原因までさかのぼれます。デバッグがしやすくなります。まとめ
- カスタム例外は
class MyError(Exception): passで作ります。 - 発生は
raise MyError("...")、捕捉はexcept MyError as e:。 - 共通の親例外を作ると、親で子もまとめて捕捉できます。
__init__+super().__init__(message)で独自の属性を持たせられます。raise 新例外 from 元例外で、原因をたどれるようにします。
カスタム例外は、エラーの種類を明確にし、処理を分けやすくするためのPythonの基本テクニックです。「Exceptionを継承」「共通の親でまとめる」という2点を押さえれば、規模が大きくなっても見通しのよいエラー処理が書けます。意味のある例外名で、エラーの原因が伝わるコードを目指しましょう。
