当生成器被包装以充当上下文管理器时,使用Python生成器的.send()

问题描述 投票:0回答:3

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
应该是什么。

python generator
3个回答
7
投票

@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
.throw
在上下文中引发异常,并且
.send
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")

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)  
    

0
投票

@MisterMiyagi 的解决方案工作得很好。谢谢。

除此之外,第一个解决方案可能不是 Python 常识。可能很难理解为什么需要第二个

yield
并且无法获得审查批准。 通过定义生成器的第二个解决方案似乎有点太长了。

结果,我最终得到了

yield
这样的复合对象的第三种解决方案。

from contextlib import contextmanager
from dataclasses import dataclass


@dataclass
class Context:
    generated: object
    exec_result: int = None


@contextmanager
def gen():
    ctx = Context(generated="xyz")
    yield ctx
    if ctx.exec_result is None:
        print("  No result this time")
    else:
        print("  Receive result:", ctx.exec_result)


with gen() as g:
    print("Acquired:", g.generated)
    g.exec_result = 1000


with gen() as g:
    print("Acquired:", g.generated)
© www.soinside.com 2019 - 2024. All rights reserved.