异常、上下文管理器与资源管理
本节目标
查询 Python 异常传播、异常链、异常组、上下文管理器和动态资源清理。
异常用于把失败从发生点传递给能够决定处理策略的一层;上下文管理器则把资源的取得和释放配对。本文以 Python 3.14 异常教程、try 语句参考、with 语句参考、contextlib 文档和异常组文档为准。属性访问的异常边界可接续装饰器、描述符、property 与属性访问,模块导入失败的归属可接续模块、包与导入系统。
异常层级与捕获边界
异常教程区分 BaseException 与通常业务错误使用的 Exception。应用代码应优先捕获能恢复或转换的具体异常,或在确有共同恢复策略时捕获 Exception;不要把 BaseException 当作普通错误边界,以免吞掉 SystemExit、KeyboardInterrupt 等解释器控制流。
try:
parse(source)
except ValueError:
recover_from_input()
此处的所有权是调用方决定如何恢复;不能恢复时应让异常继续传播,而不是以宽泛捕获伪装成功。
raise、传播与回溯
try 语句参考规定异常沿调用栈寻找匹配的处理器。不带参数的 raise 会重新抛出当前正在处理的异常,并保留其回溯;它适合在记录或局部清理之后继续交由外层决定。
try:
load_config()
except OSError:
log_failure()
raise
记录者不因此拥有恢复责任;在 except 块外使用裸 raise 没有活动异常,会失败。
try、except、else 与 finally
依据 try 语句参考,except 仅处理匹配异常,else 只在 try 套件正常完成且未发生异常时运行,finally 则在离开该语句前执行。
try:
value = read_value()
except ValueError:
value = default_value()
else:
validate(value)
finally:
close_trace()
finally 负责必须完成的收尾,不应以 return、break 或新异常无意覆盖原有控制流;资源所有权最好交给 with。
异常上下文、显式链与注释
异常教程说明处理另一个异常时再抛出会形成上下文;raise ... from error 则显式设置因果链。add_note() 可把阶段信息追加到异常诊断中,不必改变异常类型或消息契约。
try:
parse(text)
except ValueError as error:
raise RuntimeError("configuration failed") from error
转换层拥有面向调用方的高层错误接口,同时应保留原因为诊断所用;只有明确要隐藏原因时才使用 from None。
自定义异常与错误接口
异常教程建议异常类型从 Exception 派生。自定义类型应表达稳定的领域失败,并把调用方要检查的数据作为明确属性,而非要求调用方解析字符串。
class ConfigError(Exception):
def __init__(self, key: str) -> None:
self.key = key
super().__init__(f"missing: {key}")
类型作者拥有属性和消息的兼容边界;调用方应捕获该语义类型,不应依赖底层实现偶然抛出的异常。
ExceptionGroup 与 except*
异常组文档定义 ExceptionGroup 可携带多个 Exception 子异常。except* 从组中抽取匹配子组进行处理,未匹配部分仍按异常组语义继续传播;同一 try 中不要把 except 与 except* 混用。
try:
raise ExceptionGroup("batch", [ValueError("x"), TypeError("y")])
except* ValueError as errors:
report_invalid(errors)
处理器只拥有自己匹配的子组;忽略剩余子组会把未处理的失败错误地当作已经恢复。
__enter__ 与 __exit__ 协议
with 语句参考将 __enter__ 的返回值绑定给 as 目标,并在退出时调用 __exit__。__exit__ 返回真值会抑制传入的异常;只有上下文管理器确实已经处理该异常时才应这么做。
with open("report.txt") as stream:
content = stream.read()
from contextlib import ExitStack, contextmanager
events: list[str] = []
class Resource:
def __enter__(self) -> str:
events.append("enter")
return "resource"
def __exit__(self, exception_type, exception, traceback) -> bool:
name = exception_type.__name__ if exception_type is not None else "none"
events.append(f"exit:{name}")
return False
try:
with Resource() as resource:
events.append(f"use:{resource}")
raise RuntimeError("use failed")
except RuntimeError:
pass
try:
try:
raise ValueError("parse")
except ValueError as error:
raise RuntimeError("report") from error
except RuntimeError as error:
chain = f"{type(error.__cause__).__name__}->{type(error).__name__}"
noted = ValueError("invalid")
noted.add_note("stage=parse")
try:
raise ExceptionGroup("mixed", [ValueError("value"), TypeError("type")])
except* ValueError as group:
value_count = len(group.exceptions)
except* TypeError as group:
type_count = len(group.exceptions)
@contextmanager
def named(name: str):
stack_events.append(f"enter:{name}")
try:
yield name
finally:
stack_events.append(f"exit:{name}")
stack_events: list[str] = []
with ExitStack() as stack:
stack.enter_context(named("a"))
stack.enter_context(named("b"))
print(f"events={','.join(events)}")
print(f"chain={chain}")
print(f"note={noted.__notes__[0]}")
print(f"group=value:{value_count},type:{type_count}")
print(f"stack={','.join(stack_events)}")
events=enter,use:resource,exit:RuntimeError
chain=ValueError->RuntimeError
note=stage=parse
group=value:1,type:1
stack=enter:a,enter:b,exit:b,exit:a
Resource.__exit__ 看到 RuntimeError 后返回 False,所以异常仍由外层 except 所有;不要把清理成功误写成异常已经恢复。
contextmanager 生成器适配
contextlib 文档中的 @contextmanager 把生成器适配为上下文管理器。生成器必须只 yield 一次:yield 前是进入,yield 后或 finally 中是退出,with 块的异常会在该暂停点注入生成器。
from contextlib import contextmanager
@contextmanager
def opened(name: str):
handle = open(name)
try:
yield handle
finally:
handle.close()
生成器函数拥有它打开的 handle,因而必须在 finally 中关闭;多次 yield 或吞掉未知异常会破坏 with 的退出契约。
ExitStack 与动态资源组合
contextlib 文档提供的 ExitStack 允许在运行时逐个注册上下文管理器或回调。ExitStack 按后进先出(LIFO)顺序调用已注册的退出回调,因此后取得的资源先释放。
from contextlib import ExitStack
with ExitStack() as stack:
first = stack.enter_context(open("first.txt"))
second = stack.enter_context(open("second.txt"))
栈拥有已成功注册的资源;若后续获取失败,退出栈仍会逆序清理之前的资源,调用方不应再重复关闭它们。
异常传播与资源释放检查
异常教程要求只处理拥有恢复策略的预期异常,with 语句参考则在 __enter__ 成功返回后,保证 __exit__ 位于所有离开 with 块的路径上。宽泛捕获后继续运行、在 finally 中覆盖原失败,或让同一资源同时由多个层级负责关闭,都会破坏对应的传播与所有权边界。
try:
process()
except Exception as error:
raise RuntimeError("process failed") from error
转换层只将底层错误改写为自己的高层接口,仍应保留因果链;资源方面也只释放本层创建的对象。异步上下文管理器属于异步协议主题,不在本章混入。