You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 15:03:10