__enter__方法中return与yield导致with语句行为差异的技术问询
__enter__用yield和return会有不同行为? 你遇到的这个“反直觉”现象,核心原因其实和Python生成器的惰性执行特性直接相关,咱们一步步拆解来看:
先复盘你的代码与现象
第一个版本的代码(用yield):
class TestWith: def __init__(self, val): self.val=val def __enter__(self): print("Entered!") yield # 这里用yield代替return def __exit__(self, exc_type, exc_val, exc_tb): print(f"In exit:, type: {exc_type}, val: {exc_val}, tb: {exc_tb}") with TestWith(4): print("printme")
运行结果完全没打印Entered!,直接输出了printme和退出信息;而把yield换成return后,Entered!就正常打印了。
关键原因:生成器函数的特殊逻辑
Python里有个规则:只要函数体里写了yield,它就不再是普通函数,而是生成器函数。生成器函数最特别的点在于:
调用生成器函数时,不会立刻执行函数体里的代码,只会返回一个生成器迭代器对象;只有当你主动去迭代这个对象(比如用
next()、for循环遍历)时,函数体才会开始执行,直到遇到yield暂停。
再结合上下文管理器的执行流程看:
- 执行
with TestWith(4):时,先创建TestWith实例 - 调用实例的
__enter__方法 - 拿到
__enter__的返回值(如果用as就绑定变量),进入with块执行代码 - with块执行完后调用
__exit__
当__enter__是生成器函数时,步骤2调用它只会得到一个生成器对象,但with语句不会自动去迭代这个生成器,所以__enter__里的print("Entered!")根本没机会运行,直接跳到步骤3执行with块的代码了。
而换成return后,__enter__变回普通函数:调用它时会立刻执行函数体,先打印Entered!,再返回None,之后才进入with块——这就符合你一开始的预期了。
额外补充:想用yield实现上下文管理器怎么办?
如果想利用yield的简洁性写上下文管理器,Python官方提供了contextlib.contextmanager装饰器,专门干这个事,正确用法是这样的:
from contextlib import contextmanager @contextmanager def test_with(val): # 这里是__enter__的逻辑 print("Entered!") try: yield # 这里的yield用来分隔进入和退出逻辑,with块的代码会在这里执行 finally: # 这里是__exit__的逻辑 print(f"In exit") with test_with(4): print("printme")
这个装饰器会自动帮你把生成器转换成符合上下文管理器协议的对象,自动处理生成器的迭代:调用时执行到yield前的代码(进入逻辑),with块执行完后再执行yield后的代码(退出逻辑)。
内容的提问来源于stack exchange,提问作者antonpuz

