跳到主要内容
Cowers://
全部文章
Python

Python 自定义异常:设计清晰的错误体系

说明何时需要自定义异常,以及异常继承、错误上下文和业务异常体系的设计方法。

为什么需要自定义异常

内置异常(ValueErrorIOError 等)是通用的,无法满足业务场景的三类需求:

问题 说明
语义不清晰 ValueError 无法区分"年龄是负数"还是"邮箱格式错了"
无法附加上下文 需要携带错误码、用户 ID、请求 URL 等业务数据
无法精确捕获 except ValueError 会捕获所有 ValueError,包括不想处理的

继承体系

Python 异常继承体系:BaseException 之下分出 SystemExit(不被普通 except 捕获)与 Exception;Exception 之下是 ValueError、IOError、RuntimeError 等内置异常,自定义的 MyError 也挂在这一层。

IMPORTANT 继承 Exception 而非 BaseException SystemExitKeyboardInterrupt 继承自 BaseException,不该被普通业务代码捕获。自定义异常始终继承 Exception


定义方式

最简定义

class InsufficientFundsError(Exception):
    pass

两行即可,已是完整的自定义异常。

携带自定义属性

class InsufficientFundsError(Exception):
    def __init__(self, amount, balance):
        self.amount = amount
        self.balance = balance
        self.shortfall = amount - balance
        super().__init__(
            f"尝试取款 {amount} 元,余额仅 {balance} 元,差额 {self.shortfall} 元"
        )
    def __str__(self):
	    return '这是print打印异常输出的信息'

TIP 务必调用 super().__init__(message) 这决定了 str(e)repr(e) 的输出内容,不调用则打印异常时没有信息。


抛出与捕获

基本用法

def withdraw(amount, balance):
    if amount > balance:
        raise InsufficientFundsError(amount, balance)
 
try:
    withdraw(500, 100)
except InsufficientFundsError as e:
    print(e.shortfall)  # 400
    print(str(e))       # 尝试取款 500 元,余额仅 100 元,差额 400 元

except 的捕获机制

NOTE 核心原理 except 底层使用 isinstance() 判断,捕获的是类型身份(继承关系),与属性无关

isinstance(InsufficientFundsError(), InsufficientFundsError)  # True
isinstance(InsufficientFundsError(), BankError)               # True  ← 父类也能捕获
isinstance(InsufficientFundsError(), ValueError)              # False ← 无继承关系,捕获不到

方向规则:只能子类→父类,不能反向。

# ✅ raise 子类 → except 父类:可以捕获
try:
    raise InsufficientFundsError(500, 100)
except BankError as e:
    print(type(e))      # <class 'InsufficientFundsError'>
    print(e.shortfall)  # 400 ← 子类属性仍然可以访问
 
# ❌ raise 父类 → except 子类:捕获不到,异常向外抛出
try:
    raise Exception("父类异常")
except InsufficientFundsError:
    print("不会执行")
 
# ❌ raise 无关类 → except 自定义类:捕获不到
try:
    raise ValueError("无关异常")
except InsufficientFundsError:
    print("不会执行")

WARNING 没有 raise 或类型对不上,except 直接跳过 except 是守门人,只认"血统"(继承链),不管 try 里写了什么,只看最终 raise 出来的类型。


分级捕获(工程实践)

建立模块异常体系

# 模块基类
class BankError(Exception):
    """银行业务异常基类"""
    pass
 
# 具体子类
class InsufficientFundsError(BankError):
    def __init__(self, amount, balance):
        self.shortfall = amount - balance
        super().__init__(f"余额不足,差额 {self.shortfall}")
 
class AccountFrozenError(BankError):
    def __init__(self, account_id):
        self.account_id = account_id
        super().__init__(f"账户 {account_id} 已被冻结")
 
class DailyLimitExceededError(BankError):
    pass

分级捕获

try:
    transfer(...)
except InsufficientFundsError as e:   # 最具体,写最前面
    notify_user(e.shortfall)
except BankError as e:                 # 兜底:所有银行业务异常
    log_error(e)
except Exception as e:                 # 最后防线:未预期异常
    alert_oncall(e)

IMPORTANT except 从上到下顺序匹配,最具体的写最前面。


异常链

表达"因为 X 所以发生了 Y":

def load_config(path):
    try:
        with open(path) as f:
            return json.load(f)
    except FileNotFoundError as e:
        raise ConfigError(f"配置文件不存在: {path}") from e
        # from None  ← 不想暴露底层异常时使用

from e 将原始异常挂在 __cause__ 上,traceback 显示完整因果链,调试时非常有用。


工程实践建议

  • 一个模块一个基类XxxError(Exception),具体异常继承它
  • 异常类放在单独文件:通常命名 exceptions.py,避免循环导入
  • 写 docstring:说明"什么情况下抛出"
  • 不要滥用:内置异常语义已够精确时(如 TypeError)无需自定义,自定义异常的价值在于业务语义

速查总结

场景 结果
raise 子类except 父类 ✅ 捕获成功
raise 父类except 子类 ❌ 捕获失败,异常向外抛
raise 无关类except 自定义 ❌ 捕获失败,异常向外抛
捕获后访问子类属性 ✅ 可以,e 仍是子类完整实例