异常链与 raise
raise 语句是 Python 中主动触发异常的唯一方式。它不仅可以抛出新的异常,还能通过异常链机制保留原始错误上下文,让调试者看清问题的完整来龙去脉。
raise 基本语法
raise 的唯一参数必须是一个异常实例或异常类(派生自 BaseException)。如果传入的是类,Python 会自动调用其无参构造函数来创建实例。
# 抛出异常实例
raise NameError("HiThere")
# Traceback: NameError: HiThere
# 传入类,自动实例化
raise ValueError
# 等价于 raise ValueError()
# 带参数的自动实例化
raise RuntimeError("配置加载失败")
异常实例的构造参数会被存入 .args 属性,并在 Traceback 的最后部分显示为错误详情。因此,构造异常时传入的字符串应当清晰描述问题。
def load_file(path):
if not path.endswith(".txt"):
raise ValueError(f"只支持 .txt 文件,收到:{path}")
with open(path) as f:
return f.read()
load_file("data.csv") # ValueError: 只支持 .txt 文件,收到:data.csv
重新抛出异常
在 except 子句中,单独的 raise 语句(不带参数)会重新抛出当前正在处理的异常。这在需要记录或清理后再把异常交给上层处理时非常有用。
try:
raise NameError("HiThere")
except NameError:
print("异常掠过,记录日志...")
raise # 重新抛出同一个 NameError
重新抛出保留了原始的 Traceback,不会丢失任何上下文信息。这与 raise NameError("HiThere") 不同——后者会创建一个新的异常实例和新的抛出点。
try:
risky_call()
except Exception as e:
logger.error(f"操作失败:{e}")
raise # 原样抛出,调用者看到原始 Traceback
raise from:显式异常链
当一个异常是另一个异常的直接后果时,可以使用 raise ... from 建立显式的因果关系。这在异常转换场景中尤为重要——例如将低层异常包装为高层业务异常,同时保留原始原因。
def func():
raise ConnectionError("数据库连接超时")
try:
func()
except ConnectionError as exc:
raise RuntimeError("无法初始化服务") from exc
Traceback 会显示两个异常,并用 The above exception was the direct cause of the following exception: 标明因果关系:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "<stdin>", line 2, in func
ConnectionError: 数据库连接超时
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
RuntimeError: 无法初始化服务
异常链信息存储在异常实例的 __cause__ 属性中。通过 raise ... from 转换异常,调用者既能捕获高层的 RuntimeError 做业务处理,也能通过 __cause__ 访问底层的 ConnectionError 做技术诊断。
try:
raise RuntimeError("服务启动失败") from ConnectionError("端口被占用")
except RuntimeError as e:
print(e) # 服务启动失败
print(e.__cause__) # 端口被占用
print(type(e.__cause__)) # <class 'ConnectionError'>
隐式异常链
如果在 except 子句处理异常的过程中又触发了新的异常,Python 会自动将两个异常关联起来,形成隐式异常链。Traceback 中用 During handling of the above exception, another exception occurred: 连接。
try:
open("database.sqlite")
except OSError:
raise RuntimeError("无法处理错误")
这里 RuntimeError 不是 OSError 的直接后果,而是在处理 OSError 的过程中意外发生的。隐式链通过 __context__ 属性存储前一个异常。
from None:禁用异常链
有时你不希望暴露底层异常的细节,可以使用 from None 来禁用自动异常链,让 Traceback 只显示新的异常。
try:
open("secret.key")
except OSError:
raise RuntimeError("认证失败") from None
输出将只有 RuntimeError: 认证失败,不会提及文件不存在的细节。这在安全敏感场景中有用,但日常调试中应谨慎使用,避免隐藏真正的问题根源。
assert 语句
assert 是一种特殊的异常触发机制,用于检查程序内部的不变式。它只在调试模式下有效——当 Python 以优化模式运行(python -O)时,所有 assert 语句会被移除。
def divide(a, b):
assert b != 0, "除数不能为零"
return a / b
divide(10, 0) # AssertionError: 除数不能为零
assert 的语法是 assert condition [, message]。如果 condition 为假,则抛出 AssertionError,可选的 message 作为异常参数。
assert 适用于检查"绝不应该发生"的编程错误,而不是处理外部输入或运行时环境的不确定性。用户输入错误应当用 if/raise 处理,只有程序逻辑自身的矛盾才适合用 assert。
# ✅ 正确使用:检查内部不变式
def process_list(items):
assert isinstance(items, list), "内部错误:期望 list 类型"
assert len(items) > 0, "内部错误:列表不应为空"
return sum(items) / len(items)
# ❌ 错误使用:用 assert 做输入校验
def user_login(password):
assert len(password) >= 8, "密码太短" # 危险!优化模式下被移除
# 应当用:
if len(password) < 8:
raise ValueError("密码至少需要8个字符")
异常链的遍历
Python 3.12 中,可以通过 __cause__(显式链)和 __context__(隐式链)属性遍历完整的异常链,实现自定义的错误报告逻辑。
def print_exception_chain(exc):
depth = 0
while exc:
indent = " " * depth
print(f"{indent}{type(exc).__name__}: {exc}")
exc = exc.__cause__ or exc.__context__
depth += 1
try:
try:
int("not a number")
except ValueError as e:
raise RuntimeError("数据转换失败") from e
except RuntimeError as e:
print_exception_chain(e)
# ValueError: invalid literal for int() with base 10: 'not a number'
# RuntimeError: 数据转换失败