Python 异常类怎么自定义,什么时候该用自定义异常?
Python 中如何自定义异常类?什么时候该用自定义异常而不用内置异常?
这题考察两点:
-
写一个自定义异常类有多简单(语法)
-
能说出什么时候必须“造轮子”(设计意识)
🧱 自定义异常:语法上极其简单¶
Python 里自定义异常,只需要继承 Exception(或它的子类)。
最简形式一行代码搞定:
然后就可以像内置异常一样使用:
def get_order(order_id):
order = db.find(order_id)
if not order:
raise OrderNotFoundError(f"订单 {order_id} 不存在")
✅ 这就完成了一个自定义异常,它和 ValueError、RuntimeError 是同一个家族的。
🧩 如果只是这样,为什么要自定义?¶
因为名字就是语义。看两段代码对比:
❌ 只用内置异常
def cancel_order(order_id):
order = get_order(order_id)
if order.status == "shipped":
raise RuntimeError("已发货的订单不能取消")
...
调用方只能捕获宽泛的 RuntimeError,很容易误伤其它运行时错误,也没法区分到底是“订单不存在”还是“订单状态不允许”。
✅ 使用自定义异常
class OrderCancelNotAllowedError(Exception):
pass
def cancel_order(order_id):
order = get_order(order_id)
if order.status == "shipped":
raise OrderCancelNotAllowedError(
f"订单 {order_id} 已发货,无法取消"
)
调用方可以精确捕获:
try:
cancel_order("X123")
except OrderNotFoundError:
print("订单号错了")
except OrderCancelNotAllowedError:
print("这个订单不能取消,提示用户")
异常类名就是一份可读的文档,也把错误类型变成了一等公民。
🎨 自定义异常还能做什么?——携带更多上下文¶
自定义异常可以带任意属性,方便上层处理或记录日志:
class PaymentFailedError(Exception):
def __init__(self, order_id: str, amount: float, reason: str):
super().__init__(
f"订单 {order_id} 支付 {amount} 元失败: {reason}"
)
self.order_id = order_id
self.amount = amount
self.reason = reason
# 使用
raise PaymentFailedError("X123", 99.9, "余额不足")
上层捕获时,可以直接用 e.order_id 拿到订单号做后续补偿逻辑,而不用去解析字符串。
📦 什么时候该用自定义异常?——一个判断法则¶
典型层次结构长这样:

顶层捕获 AppError 可以兜底所有业务异常,与 Exception(系统级)区分开,防止误吞 KeyboardInterrupt 等。
🔧 进阶:异常链和 raise ... from¶
当你捕获一个底层异常并想转换为自定义异常时,记得保留“案发现场”:
class DataAccessError(Exception):
pass
def load_config():
try:
with open("config.yaml") as f:
...
except FileNotFoundError as e:
raise DataAccessError("配置文件丢失") from e
from e 会设置 cause,打印堆栈时能看到完整的因果链,定位问题非常舒服。
🧪 最佳实践¶
-
✅ 继承自
Exception,不要直接继承BaseException(那是给系统级退出信号用的)。 -
✅ 异常名以
Error结尾,一目了然。 -
✅ 把异常定义在靠近使用处,比如一个
exceptions.py模块,集中管理。 -
✅ 只在真正需要区分处理时才自定义,一两处就用内置异常也没问题。
-
✅ 覆写
init和str,提供友好的错误消息。
✅ 总结¶
“自定义异常的价值不在于创造新语法,而在于用类名传递业务语义、用属性承载错误上下文,让上层代码能精确、优雅地处理不同的错误分支,把异常从‘报错消息’提升为‘领域语言’。”