【Python】カスタム例外(独自例外)の作り方|Exception継承・階層・raise from

【Python】カスタム例外(独自例外)の作り方|Exception継承・階層・raise from Python

PythonにはValueErrorKeyErrorといった組み込みの例外がありますが、自分のプログラム特有のエラーには、独自の例外(カスタム例外)を作るのが効果的です。たとえば「在庫が足りない」「APIの認証に失敗した」といったエラーにOutOfStockErrorAuthErrorのような意味のある名前を付けると、何が起きたのかがコードから一目で分かり、エラーの種類ごとに処理を分けやすくなります。

カスタム例外は、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

実機でも、ValidationErrorraiseして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を継承したNotFoundErrorraiseしても、親の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_code404という独自の属性を取り出せました。super().__init__(message)でメッセージを親(Exception)に渡すことで、str(e)でメッセージが取れるようになります。そのうえでself.status_code = status_codeのように属性を追加すれば、例外を捕捉した側でその情報を使って処理を分けられます。HTTPのステータスコードや、どの値でエラーになったかなど、対処に必要な情報を例外に持たせると便利です。

raise from で元の例外を残す

ある例外を捕まえて別の例外に変換して投げ直すとき、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 元の例外を付けて、原因をたどれるようにします。

よくある質問

Qカスタム例外はどう作りますか?
Aclass MyError(Exception): passのように、Exceptionを継承したクラスを作るだけです。raise MyError("メッセージ")で発生させ、except MyError as e:で捕捉します。プログラム特有のエラーに意味のある名前を付けられ、エラーの種類ごとに処理を分けやすくなります。
Qなぜ組み込みの例外ではなくカスタム例外を作るのですか?
Aプログラム特有の状況に専用の名前を付けることで、エラーの原因が明確になり、except 独自例外:でその状況だけを狙って処理できるためです。ValueErrorのような汎用的な例外だと、どんな原因のエラーかが区別しにくくなります。
Q複数のカスタム例外をまとめて捕捉するには?
A共通の親例外(たとえばAppError)を作り、個別の例外をその子として定義します。except AppError:とすれば、子の例外もまとめて捕捉できます。exceptは指定したクラスとそのサブクラスをすべて受けるためです。
Q例外にステータスコードなどの情報を持たせるには?
A__init__を定義し、super().__init__(message)でメッセージを親に渡したうえで、self.status_code = status_codeのように属性を追加します。捕捉した側でe.status_codeとして取り出し、処理を分けられます。
Qraise fromは何のために使いますか?
Aある例外を別の例外に変換して投げ直すときに、元の例外の情報を残すためです。raise AppError(...) from 元の例外とすると、__cause__に元の例外が保持され、エラー表示でも本当の原因までさかのぼれます。デバッグがしやすくなります。

まとめ

  • カスタム例外はclass MyError(Exception): passで作ります。
  • 発生はraise MyError("...")、捕捉はexcept MyError as e:
  • 共通の親例外を作ると、親で子もまとめて捕捉できます。
  • __init__super().__init__(message)独自の属性を持たせられます。
  • raise 新例外 from 元例外で、原因をたどれるようにします。

カスタム例外は、エラーの種類を明確にし、処理を分けやすくするためのPythonの基本テクニックです。「Exceptionを継承」「共通の親でまとめる」という2点を押さえれば、規模が大きくなっても見通しのよいエラー処理が書けます。意味のある例外名で、エラーの原因が伝わるコードを目指しましょう。