当Python生成器被封装成上下文管理器时,可以使用生成器的.send()方法。

6

Python的contextlib提供了将生成器转换为上下文管理器的包装器:

from contextlib import contextmanager

@contextmanager
def gen():
    yield

with gen() as cm:
    ...

生成器提供了向刚刚产生值的生成器发送值的功能:

def gen():
    x = yield
    print(x)

g = gen()
next(g)
g.send(1234) # prints 1234 and raises a StopIteration because we have only 1 yield

有没有办法同时获取这两种行为?我希望将一个值传递到我的上下文管理器中,以便在处理__exit__时使用。所以类似这样:

from contextlib import contextmanager

@contextmanager
def gen():
    x = yield
    # do something with x

with gen() as cm:
    # generator already yielded from __enter__, so we can send
    something.send(1234)

我不确定这是否是一个好的/合理的想法。 我觉得它会破坏一些抽象层,因为我会假设上下文管理器是作为包装的生成器实现的。 如果这是可行的想法,我不确定 something 应该是什么。

为什么不让上下文返回一个单独的生成器(可能在上下文生成器内设置结果)?将上下文管理器编写为类而不是包装的生成器也会使您想要做的事情更加容易。您是否需要主动设置一个“@contextmanager”生成器的特定设置,使其在上下文中可“send”呢? - MisterMiyagi
2个回答

6
@contextmanager 底层的生成器可以通过其 gen 属性直接访问。由于生成器无法访问上下文管理器,因此必须在上下文之前存储后者。
from contextlib import contextmanager

@contextmanager
def gen():
    print((yield))  # first yield to suspend and receive value...
    yield           # ... final yield to return at end of context

manager = gen()     # manager must be stored to keep it accessible
with manager as value:
    manager.gen.send(12)

生成器中有恰好正确数量的yield点是非常重要的,@contextmanager可以确保在退出上下文后生成器已用尽。

@contextmanager将在上下文中触发抛出的异常,并在完成时发送None,底层生成器可以侦听这些事件:

@contextmanager
def gen():
    # stop when we receive None on __exit__
    while (value := (yield)) is not None:
        print(value)

在许多情况下,将上下文管理器实现为自定义类可能更容易。这避免了使用相同通道发送/接收值和暂停/恢复上下文时可能出现的复杂情况。
class SendContext:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass

    def send(self, value):
        print("We got", value, "!!!")


with SendContext() as sc:
    sc.send("Deathstar")

1
太棒了!同意把CM作为其自己的类实现对于这个预期使用来说会更容易,我只是想知道你指出的另一种方法(使用.gen属性)是否也可行。背景是一个接受用户提供的生成器并在内部将其用作上下文管理器的库。因此,要求用户编写类而不是生成器将需要不同程度的工作/更改。 - LoveToCode

0

我认为这是不可能的,而且可能有更好的方法来实现你想要的。

contextmanager 的实现方式在 PEP 344 中有所描述,关于 with 语句。contextmanager 在用户的生成器函数上调用 next 方法两次,一次在 __enter__ 方法中,一次在 __exit__ 方法中,都没有传入任何值。

你想要实现的方式大致如下(未经测试):

def create_context_manager(value_you_want_to_pass):
    @contextmanager
    def gen():
        do_something_with(value_you_want_to_pass)
        yield the_object_for_with_statement
        do_something_else_with(value_you_want_to_pass)
    return gen

with create_context_manager(1234) as cm:
    do_something_with(cm)  
    

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接