Python @contextmanager装饰的上下文管理器为何会屏蔽异常?
问题原因
你用@contextmanager实现的上下文管理器异常被屏蔽,核心原因是在finally代码块中写了return语句,这触发了Python本身的异常屏蔽规则。
底层逻辑说明
- 被
@contextmanager装饰的生成器函数,执行到yield关键字时会暂停运行,将控制权交给with块内的业务代码。 - 当
with块内抛出异常时,该异常会被重新注入到生成器暂停的yield位置,沿着生成器的调用栈向外传播。 - Python语法有明确规则:如果
finally块中存在return、break这类会跳出当前代码块的控制流语句,try结构中未处理的异常会被直接丢弃,不会继续向外抛出。
你可以用最简单的普通函数验证这个规则:
def demo(): try: raise ValueError("测试异常") finally: return
调用上述demo()函数时,不会抛出任何异常,ValueError会被finally里的return直接吞掉。
回到你的上下文管理器代码:with块抛出MyException后,异常回到生成器的yield断点,进入finally块执行return,直接把待传播的MyException丢弃,生成器正常执行结束,外层的except MyException分支自然捕获不到异常,就会走到打印How did I get here ???的逻辑。
为什么nullcontext不会有问题
nullcontext是标准库提供的空上下文管理器,进入和退出阶段不会做任何干扰异常传播的操作,with块抛出的异常会原封不动向外传递,因此能被外层except分支正常捕获。
正确写法
如果不需要在上下文管理器退出阶段做特殊的清理逻辑,直接去掉finally块和无意义的return即可:
@contextmanager def annotate(): yield None
如果确实需要用finally块做资源释放等清理操作,只在finally块中写清理逻辑即可,绝对不要在finally块中写return/break这类会中断控制流的语句,避免异常被意外屏蔽。
内容的提问来源于stack exchange,提问作者Boaz Nahum
相关产品推荐
相关产品推荐

