Python|カスタム例外クラスの階層設計ミスを防ぐ実践ポイント
この記事のポイント
- カスタム例外クラスの階層整理がレビューできるようになる
- 例外設計の「役割崩壊パターン」を具体例から読み取れる
- 開発者がやりがちな例外設計の過剰化・不足化を指摘できる
そもそもカスタム例外とは
Pythonでは標準例外クラスに加え、独自のカスタム例外クラスを自由に定義できます。
class SaveLogError(Exception):
pass
主に以下の目的で利用されます。
- 責務ごとのエラーレイヤ整理
- 外部向けと内部用の区別
- 上位層のハンドリング簡略化
- 例外型に設計意図を付与
カスタム例外の正しい設計は、エラーハンドリング責務そのものの表現になります。
なぜこれをレビューするのか
現場の実装で頻発する失敗は次の通りです。
- 無計画に乱立したカスタム例外
- 例外階層が平坦すぎる
- 抽象例外の意味不明化
- 障害調査困難な例外情報設計
レビューアーは「例外の階層設計=責務境界の写像」であることを常に意識します。
レビューアー視点
- 例外型の粒度が適切か
- 抽象例外の役割が明確か
- 例外名が設計意図を表現しているか
- ハンドリング単位が読み取れるか
- 利用箇所が整理されているか
開発者視点
- 役割単位の階層化を意識する
- 下位層→上位層で例外型を変換設計
- 例外ラップ時はfromで原因保持
- 不要にException直継承しない
- キャッチ側で自然に処理分岐できる設計にする
良い実装例
なぜこの実装が良いのか
- 技術層・業務層で階層分離されている
- 例外名が用途を自然に表現
- ハンドリング粒度を意識した設計
- 障害調査性を高める情報保持
- 無駄な深いネストを排除
# exception_definitions.py
class ApplicationError(Exception):
"""アプリケーション共通基底例外"""
pass
class DatabaseError(ApplicationError):
"""インフラ層例外"""
pass
class SaveLogError(ApplicationError):
"""ビジネス層例外"""
pass
# db_access.py
def insert_request_log(log):
# DB書き込み処理
raise Exception("DB接続失敗") # 仮の失敗例
# request_logger.py
import logging
from exception_definitions import DatabaseError, SaveLogError
from db_access import insert_request_log
def save_request_log(log):
try:
insert_request_log(log)
except Exception as e:
raise DatabaseError("DB障害") from e
def handle_request(log):
try:
save_request_log(log)
except DatabaseError as e:
logging.error(f"DB層失敗: {e}")
raise SaveLogError("ログ保存失敗") from e
補足
障害原因はDB障害だが、ビジネス層ではSaveLogErrorのみ扱う。責務が自然に上下分離され、例外連鎖も維持されています。
レビュー観点
- 技術層/業務層の責務境界が整理されているか
- 汎用抽象例外(ApplicationErrorなど)の意味が明確か
- 例外クラス命名が利用文脈に沿っているか
- fromによる原因保持が設計全体に貫かれているか
- 冗長・過剰・不足な例外型がないか
良くない実装例: ケース1(乱立・粒度崩壊パターン)
# bad_exception_definitions.py
class RequestLogInsertFailed(Exception):
pass
class RequestLogConnectionFailed(Exception):
pass
class RequestLogTimeout(Exception):
pass
class RequestLogIntegrityError(Exception):
pass
# request_logger_too_granular.py
import logging
from bad_exception_definitions import *
def save_request_log(log):
try:
# DB処理
pass
except ConnectionError:
@Reviewer接続失敗専用例外をわざわざ作る必要は低いです。責務粒度を整理してください。 raise RequestLogConnectionFailed("接続失敗")
except TimeoutError:
@Reviewerタイムアウトも個別例外化せず、DB層抽象例外に統合できます。 raise RequestLogTimeout("タイムアウト")
except IntegrityError:
raise RequestLogIntegrityError("整合性違反")
except Exception:
@Reviewer共通の上位層抽象例外が欠落しています。ApplicationErrorなどの抽象層を設けてください。 raise RequestLogInsertFailed("保存失敗")
問題点
- 粒度過剰で分類が細かすぎる
- 上位層で例外ハンドリングが煩雑化
- 抽象例外層が無く整理性が崩壊
改善例
# good_exception_definitions.py
class ApplicationError(Exception):
pass
class DatabaseError(ApplicationError):
pass
class SaveLogError(ApplicationError):
pass
役割単位に階層整理し、発生原因の違いではなくハンドリング責務の違いで分類するのが基本方針です。
良くない実装例: ケース2(抽象例外濫用・役割不明瞭パターン)
# ambiguous_exception.py
class ApplicationException(Exception):
pass
class LogicError(ApplicationException):
pass
class SystemError(ApplicationException):
pass
class UnknownError(ApplicationException):
pass
# request_logger_ambiguous.py
import logging
from ambiguous_exception import *
def save_request_log(log):
try:
# DB処理
pass
except Exception as e:
@ReviewerSystemError・LogicErrorなど曖昧で具体的役割が不明確です。命名意図を整理してください。 raise SystemError("障害発生") from e
問題点
- 階層はあるが役割が曖昧
- システム設計の層構造と一致しない
- 抽象語ばかりで実運用での役立ちが薄い
改善例
# request_logger_responsibility.py
class ApplicationError(Exception):
pass
class DatabaseError(ApplicationError):
pass
class SaveLogError(ApplicationError):
pass
# request_logger_fixed2.py
import logging
from request_logger_responsibility import *
def save_request_log(log):
try:
# DB処理
pass
except Exception as e:
raise DatabaseError("DB層失敗") from e
抽象名よりも「設計上の責務名」で整理する方が読み手も維持者も迷いにくくなります。
観点チェックリスト
まとめ
カスタム例外の階層設計は「そのプロジェクトのエラーハンドリング思想」を如実に反映します。
粒度・階層数・命名・from保持などをレビュー観点に取り入れ、例外階層そのものを責務設計レビューの材料とする習慣が重要です。
「階層が多いほど良い」「抽象名が高尚」──そうではなく、維持者が迷わない設計粒度こそレビュー品質の核心になります。