Python条件上下文管理器实现:含yield函数非执行分支不运行问题
问题复现
测试代码如下:
import unittest from contextlib import contextmanager def outer(with_context=False): def inner(): # Some code here if with_context: with unittest.TestCase().assertRaises(KeyError): yield else: print("No context") # Some more code here if with_context: return contextmanager(inner) else: return inner no_context_func = outer() no_context_func() # "No context" does not get printed my_dict = {"valid_key": 123} context_func = outer(with_context=True) with context_func(): my_dict["valid_key"] # Works fine
运行时出现不符合预期的现象:
- 调用
no_context_func()时控制台未打印"No context",调用似乎被直接跳过,甚至无法在print("No context")行命中断点 - 删除
if with_context:分支内的代码后,"No context"可以正常打印
核心疑问:yield位于未执行的分支中,为什么仍然会影响程序执行?同时希望将outer实现为可参数化的装饰器,同时兼容普通函数、上下文管理器两种场景,避免逻辑重复。
根本原因
Python对生成器函数的判定是*函数定义阶段(语法解析时)*完成的,和运行时的分支逻辑无关:只要函数体中任意位置出现了yield关键字,不管这行代码运行时会不会被执行,这个函数都会被标记为生成器函数,调用时不会直接执行函数体代码,而是返回一个生成器对象。只有手动迭代这个生成器(比如调用next()、放在for循环里)时,函数体内的代码才会真正执行。
代码中inner函数体里存在yield语句,因此不管with_context参数是什么,inner始终是生成器函数:
- 当
with_context=True时,contextmanager装饰器会自动处理这个生成器,适配上下文管理器协议,因此可以正常用在with语句里 - 当
with_context=False时,直接返回的inner是生成器函数,调用no_context_func()只是得到了一个未迭代的生成器对象,函数体里的print、其他业务逻辑都不会执行,自然看不到输出、打不中断点。手动执行next(no_context_func())就能看到"No context"正常打印。
兼容实现方案
不需要编写两套独立的装饰器,也不会出现逻辑重复,核心思路是把公共业务逻辑抽离成独立的复用片段,再根据参数返回不同类型的包装对象即可,避免在共用的普通函数里写yield:
import unittest from contextlib import contextmanager from functools import wraps def outer(with_context=False): # 抽离公共前后置逻辑,完全避免重复 def run_common_pre(): # 此处放原inner函数分支前的所有业务逻辑 pass def run_common_post(): # 此处放原inner函数分支后的所有业务逻辑 pass if with_context: # 返回上下文管理器版本 @contextmanager def cm_wrapper(): run_common_pre() try: # 上下文管理器场景下的特殊逻辑 with unittest.TestCase().assertRaises(KeyError): yield finally: run_common_post() return cm_wrapper else: # 返回普通函数版本 def func_wrapper(*args, **kwargs): run_common_pre() # 普通函数场景下的特殊逻辑 print("No context") run_common_post() # 如果作为装饰器使用,在此处调用被装饰的原函数即可 # return wrapped_func(*args, **kwargs) return func_wrapper
如果要作为装饰器使用,只需要在外层再加一层接收被装饰对象的逻辑即可,公共的前后置代码只需要维护一份。
内容的提问来源于stack exchange,提问作者Mr-Pepe
相关产品推荐
相关产品推荐

