Python 内置了丰富的异常类,但在实际业务中,仅靠内置异常往往无法精确表达错误语义。这时就需要自定义异常类,并配合 raise 语句在恰当的时机主动抛出异常。
为什么需要自定义异常?
- 精确表达业务语义:
ValueError太宽泛,“转账时余额不足”、“API 返回的数据格式错误”这些具体错误,用自定义异常能让调用方更清楚地知道出了什么问题。 - 便于分类捕获:不同的业务异常可以继承自一个公用基类,外层用一个
except捕获所有自身业务异常,以便统一记录日志或上报监控,内层再精确处理子类异常。 - 携带额外上下文:自定义异常可以定义更多的属性和方法,例如错误码、请求参数、原始响应体等,方便排查问题。
创建自定义异常类
自定义异常类非常简单,通常是继承自 Exception(或其子类),一般只需要写一个类定义,最多实现构造函数来传入额外信息:
class PaymentError(Exception):
"""支付相关的所有异常基类"""
pass
class InsufficientBalanceError(PaymentError):
"""余额不足"""
def __init__(self, account, amount, current_balance):
self.account = account
self.amount = amount
self.current_balance = current_balance
super().__init__(
f"账户 {account} 余额不足,需要 {amount},当前余额 {current_balance}"
)
class PaymentTimeoutError(PaymentError):
"""支付超时"""
pass
约定:自定义异常类的名称通常以 Error 或 Exception 结尾,保持清晰。
主动抛出异常:raise
raise 语句用于在当前执行点立即引发异常,它可以抛出内置异常,也可以抛出自定义异常。语法有三种常见形式:
- 直接抛出异常对象:
raise ValueError("无效输入") - 抛出一个异常实例,并携带原始异常(用于异常链):
raise NewError from original_error - 在
except块中单独使用raise,将原异常重新抛出,保持原有调用栈。
主动抛出的典型场景:
- 入口参数校验不通过:函数开始处检查参数合法性,不合法立即抛出
ValueError或自定义业务异常。 - 核心业务规则违反:如库存不足、状态流转非法、权限检查失败等,主动中断流程,向上层抛出清晰异常。
- 外部依赖返回非预期结果:调用第三方 API 或数据库后,发现返回格式不符合预期,封装成自定义异常再抛出。
示例:一个转账函数
def transfer(from_account, to_account, amount):
if amount <= 0:
raise ValueError("转账金额必须为正数")
if from_account.balance < amount:
raise InsufficientBalanceError(
from_account.id, amount, from_account.balance
)
# 执行转账逻辑...
这样调用方可以在合适层级捕获并处理:
try:
transfer(a, b, 1000)
except InsufficientBalanceError as e:
print(f"转账失败:余额不足,请充值 {e.amount - e.current_balance}")
except PaymentError as e:
print(f"支付发生错误:{e}")
异常链:raise ... from
当你在 except 块中想转换为另一种异常再抛出时,推荐使用 raise NewError from original_error,这样原始异常会保留在 NewError.cause 中,调用栈信息不会丢失,调试时能看到完整因果链:
try:
# 调用某个可能失败的接口
some_service.call()
except ConnectionError as ex:
raise PaymentTimeoutError("支付网关连接超时") from ex
如果不加 from,也可以直接 raise PaymentTimeoutError(...),但上下文链会断开,不利于排查。
最佳实践
- 自定义异常层次清晰:通常一个模块或一个包会定义一个基础异常类,其他具体异常都继承自它,方便外部统一捕获。
- 携带足够上下文:错误消息要描述清楚“什么错了、为什么错、怎么修复”,复杂错误附加属性(如错误码、参数)。避免抛出
Exception("出错了")这种信息量为零的异常。 - 慎用裸
raise:在except块中如果不打算处理,一般用裸raise原样向上传播,而不是捕获后忽略或只打印日志。后者会隐藏错误,导致逻辑继续执行,引发更难排查的问题。 - 异常与正常流程分离:不要用异常来实现普通分支逻辑,性能开销大且可读性差。异常应留给真正的意外情况。
通过自定义异常和主动抛出,你的代码将从“被动报错”转向“主动守卫”,让错误更早暴露,信息更清晰,系统更健壮。