自定义异常
当内置异常无法准确表达程序特定的错误语义时,可以通过继承 Exception 创建自定义异常类。良好的自定义异常体系能让错误信息更专业、捕获更精准、调试更高效。
继承 Exception 创建异常类
自定义异常类只需继承 Exception(或其子类),通常保持简单,仅提供必要的属性和文档字符串。
class ValidationError(Exception):
"""数据校验失败时抛出。"""
pass
class ConfigError(Exception):
"""配置文件相关错误。"""
pass
# 使用
raise ValidationError("用户名字段不能为空")
异常类命名应遵循标准库的惯例,以 Error 结尾(如 ValueError、TypeError),让人一眼识别其用途。虽然异常类可以做任何普通类能做的事,但过度复杂的设计往往适得其反——异常的核心职责是携带错误信息并支持被捕获。
自定义异常参数
通过重写 __init__() 和 __str__(),可以让异常携带结构化数据,并在打印时显示更友好的消息。
class APIError(Exception):
"""HTTP API 请求错误。"""
def __init__(self, status_code, message, endpoint):
self.status_code = status_code
self.endpoint = endpoint
super().__init__(message)
def __str__(self):
return f"[{self.status_code}] {self.args[0]} (URL: {self.endpoint})"
# 使用
try:
raise APIError(404, "资源不存在", "/api/users/99")
except APIError as e:
print(e) # [404] 资源不存在 (URL: /api/users/99)
print(e.status_code) # 404
print(e.endpoint) # /api/users/99
注意在自定义 __init__() 时应当调用 super().__init__() 传入消息字符串,这样 .args 属性和默认的 __str__() 行为才能正常工作。
class FieldError(Exception):
"""表单字段错误。"""
def __init__(self, field_name, value, reason):
self.field_name = field_name
self.value = value
self.reason = reason
super().__init__(f"字段 '{field_name}' 不合法:{reason}")
try:
raise FieldError("age", "abc", "必须是整数")
except FieldError as e:
print(e) # 字段 'age' 不合法:必须是整数
print(e.field_name) # age
print(e.value) # abc
异常层次设计
为项目设计分层的异常体系,可以实现精准捕获和统一处理。顶层异常作为命名空间,让调用者选择捕获整个模块的错误或只捕获特定类型。
# 项目根异常
class ProjectError(Exception):
"""本项目所有异常的基类。"""
pass
# 模块级异常
class DataError(ProjectError):
"""数据层错误。"""
pass
class ServiceError(ProjectError):
"""业务层错误。"""
pass
# 具体异常
class RecordNotFoundError(DataError):
"""数据库记录不存在。"""
def __init__(self, table, record_id):
self.table = table
self.record_id = record_id
super().__init__(f"表 {table} 中找不到 ID={record_id} 的记录")
class PermissionDeniedError(ServiceError):
"""权限不足。"""
pass
class RateLimitError(ServiceError):
"""请求频率超限。"""
def __init__(self, retry_after):
self.retry_after = retry_after
super().__init__(f"请求过于频繁,请 {retry_after} 秒后重试")
这种层次结构的好处在于调用者可以根据需要选择捕获粒度:
# 捕获所有项目异常
try:
process_request()
except ProjectError as e:
print(f"业务错误:{e}")
# 只捕获数据层异常
try:
fetch_data()
except DataError as e:
print(f"数据错误:{e}")
# 只捕获特定异常
try:
get_user(99)
except RecordNotFoundError as e:
print(f"记录不存在:{e.table}.{e.record_id}")
异常组 ExceptionGroup(Python 3.11+)
传统 raise 一次只能抛出一个异常。当批量任务中多个步骤同时失败时,需要一种机制来报告多个不相关的异常。ExceptionGroup 打包了一个异常实例列表,使它们可以一起被抛出和捕获。
def validate_batch(data):
errors = []
for item in data:
try:
process(item)
except Exception as e:
errors.append(e)
if errors:
raise ExceptionGroup("批量处理失败", errors)
data = [1, "bad", 3, None, 5]
try:
validate_batch(data)
except ExceptionGroup as eg:
print(f"捕获到异常组:{eg.message}")
for exc in eg.exceptions:
print(f" - {type(exc).__name__}: {exc}")
使用 except* 语法可以有选择地只处理组中符合某种类型的异常。每个 except* 子句从组中提取匹配的异常,让其他异常继续传播或被后续子句处理。
def failing():
raise ExceptionGroup("group1", [
OSError(1),
SystemError(2),
ExceptionGroup("group2", [
OSError(3),
RecursionError(4)
])
])
try:
failing()
except* OSError as eg:
print(f"OS 错误数量:{len(eg.exceptions)}")
except* SystemError as eg:
print(f"系统错误数量:{len(eg.exceptions)}")
# 剩余的 RecursionError 会被重新抛出
except* 与普通 except 的关键区别在于:普通 except 捕获整个 ExceptionGroup 对象,而 except* 会拆分异常组,只提取匹配类型的子异常。嵌套在异常组中的必须是异常实例,而不是类型——因为这些异常通常是程序已经捕获并收集的实例。
用 add_note 添加上下文(Python 3.11+)
异常实例的 add_note(note) 方法允许在异常被捕获后追加注释信息。标准 Traceback 会在异常消息之后按添加顺序显示所有注释。
try:
raise TypeError("bad type")
except Exception as e:
e.add_note("发生在用户注册流程中")
e.add_note("相关用户ID:user_42")
raise
这在批量处理场景中特别有用:为每个子异常添加迭代上下文,帮助定位问题。
excs = []
for i, task in enumerate(tasks):
try:
run(task)
except Exception as e:
e.add_note(f"发生在第 {i+1} 个任务")
excs.append(e)
if excs:
raise ExceptionGroup("任务执行失败", excs)
自定义异常的最佳实践
- 始终继承
Exception(或更具体的内置异常),不要直接继承BaseException,以免意外捕获系统退出信号。 - 提供清晰的文档字符串,说明异常的触发场景。
- 在
__init__中调用super().__init__(),确保.args和字符串转换正常工作。 - 设计层次结构时遵循"宽基窄叶"原则:顶层宽泛,叶子节点具体,便于调用者按需捕获。
- 不要滥用异常做流程控制:异常应当用于"意外情况",而非预期的分支逻辑。