__enter__ 不能用 yield,必须直接 return 值;__exit__ 参数用于异常处理,无异常时全为 none;@contextmanager 安全但需单次 yield;嵌套时 __exit__ 先内后外执行,异常由首个返回 true 的拦截。

为什么加了 __enter__ 还是报错 “RuntimeError: generator didn’t yield”
这是最常见的误用:把 __enter__ 写成生成器函数(用了 yield),但上下文管理协议不要求、也不支持生成器。Python 在调用 __enter__ 时期望它返回一个值,而不是一个生成器对象。
正确做法是直接 return 要进入上下文后暴露的对象:
class DatabaseConnection:
def __init__(self, url):
self.url = url
self.conn = None
<pre class="brush:python;toolbar:false;">def __enter__(self):
self.conn = connect(self.url) # 假设 connect 是个连接函数
return self.conn # ✅ 直接 return,不是 yield
def __exit__(self, exc_type, exc_val, exc_tb):
if self.conn:
self.conn.close()
-
__enter__必须有返回值(哪怕返回None),且不能是生成器 - 如果想复用已有资源(比如单例连接池),
return那个实例即可,不用新构造 - 返回值会绑定到
as后的变量,不返回就导致as x中的x是None
__exit__ 的三个参数到底怎么用,要不要都写上
__exit__ 接收 exc_type、exc_val、exc_tb —— 分别对应异常类型、异常实例、回溯对象。它们在无异常时全为 None。是否“都要写上”,取决于你是否要处理异常。
常见场景和写法:
- 只做清理(如关闭文件、释放锁):参数名保留,但函数体里不检查异常,最后
return False(让异常继续传播) - 吞掉特定异常(比如忽略
FileNotFoundError):if exc_type is FileNotFoundError: return True - 记录异常但不压制:
logging.exception("Context exit with error"),仍return False - 绝对不要写
except:块在里面 —— 这会让__exit__变得难以调试
示例(安静地忽略连接已关闭的错误):
def __exit__(self, exc_type, exc_val, exc_tb):
try:
self.sock.close()
except OSError as e:
if exc_type is not None and "Bad file descriptor" in str(e):
return True # 吞掉这个已知噪音
return False # 其他异常照常抛出
用 @contextlib.contextmanager 装饰器替代手写方法,安全吗
安全,而且更轻量——但它底层仍是帮你生成了带 __enter__/__exit__ 的类。关键限制在于:函数体内必须有且仅有一个 yield,且 yield 的值就是 __enter__ 的返回值。
-
yield之前的部分 =__enter__逻辑(支持try/finally做前置准备) -
yield之后的部分 =__exit__逻辑(自动接收异常三元组,可判断处理) - 如果
yield后有代码但没异常发生,它会在正常退出时执行;如果有异常,也会执行,此时可通过检查sys.exc_info()或捕获yield表达式来响应
示例(带资源清理的简洁写法):
from contextlib import contextmanager <p>@contextmanager def temporary_dir(): tmp = mkdtemp() try: yield tmp # ✅ yield 一次,且只 yield 一个值 finally: rmtree(tmp) </p>
注意:装饰器生成的对象不继承自任何类,无法被 isinstance(obj, ContextManager) 检测(除非显式注册),这点在类型检查或框架集成时容易被忽略。
多个上下文嵌套时,__exit__ 的执行顺序和异常传递规则
嵌套的 with 块按“先入后出”顺序触发 __exit__:最内层最先执行,最外层最后执行。异常只由第一个返回 True 的 __exit__ 拦截;其余即使也写了处理逻辑,也不会再收到该异常。
- 如果内层
__exit__返回False(默认行为),异常会透传给外层 - 如果外层也返回
False,异常最终抛出;任一返回True,则终止传播 - 即便内层已处理异常,外层的
__exit__仍会被调用,只是三个参数全为None - 所以清理逻辑(比如关文件、解锁)一定要放在
finally块或无条件执行路径中,不能依赖异常参数判断是否执行
这点在实现带锁或事务的上下文管理器时特别关键:锁必须释放,不管有没有异常;事务必须回滚或提交,不能因为上层吞了异常就跳过。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











