【问题标题】:Send values to Python coroutine without handling StopIteration将值发送到 Python 协程而不处理 StopIteration
【发布时间】:2016-05-29 21:44:36
【问题描述】:

给定一个 Python 协程:

def coroutine():
     score = 0
     for _ in range(3):
          score = yield score + 1

我想像这样在一个简单的循环中使用它:

cs = coroutine()
for c in cs:
     print(c)
     cs.send(c + 1)

...我希望打印出来

1
3
5

但实际上,我在yield score + 1 线上遇到了一个异常:

 TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

如果我手动调用next,我可以让它工作:

c = next(cs)
while True:
    print(c)
    try:
        c = cs.send(c + 1)
    except StopIteration:
        break

但我不喜欢我需要使用 try/except,因为生成器通常非常优雅。

那么,有没有办法在不显式处理StopIteration 的情况下使用这样的有限协程?我很高兴同时更改生成器和迭代它的方式。

第二次尝试

Martijn 指出for 循环和我对send 的调用都推进了迭代器。很公平。那么,为什么我不能在协程循环中使用两个 yield 语句来解决这个问题?

def coroutine():
    score = 0
    for _ in range(3):
        yield score
        score = yield score + 1

cs = coroutine()
for c in cs:
    print(c)
    cs.send(c + 1)

如果我尝试这样做,我会得到同样的错误,但在 send 行。

0
None
Traceback (most recent call last):
  File "../coroutine_test.py", line 10, in <module>
    cs.send(c + 1)
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

【问题讨论】:

  • for-循环协程并没有什么意义。与协程交互的方式与与普通生成器的正常交互方式完全不同。
  • yield score + 1 返回 None 并将其分配给 score,因此下一次迭代尝试将 1 添加到 None

标签: python generator coroutine


【解决方案1】:

是的,对于协程,您通常必须首先使用next() 调用来“启动”生成器;它将导致生成器函数执行代码,直到第一个yield。您的问题主要是您使用的是for 循环,但是,它也使用next() ,但不发送任何内容。

您可以在协程中添加一个额外的 yield 以捕获第一个启动步骤,并从 PEP 342 添加 @consumer 装饰器(针对 Python 2 和 3 进行了调整):

def consumer(func):
    def wrapper(*args,**kw):
        gen = func(*args, **kw)
        next(gen)
        return gen
    wrapper.__name__ = func.__name__
    wrapper.__dict__ = func.__dict__
    wrapper.__doc__  = func.__doc__
    return wrapper

@consumer
def coroutine():
    score = 0
    yield
    for _ in range(3):
        score = yield score + 1

您仍然必须使用 while 循环,因为 for 循环无法发送:

c = 0
while True:
    try:
        c = cs.send(c + 1)
    except StopIteration:
        break
    print(c)

现在,如果您希望它与for 循环一起工作,您必须了解当您进入循环时来自for 循环的next() 调用何时进入。当.send() 恢复生成器时,yield 表达式返回发送的值,然后生成器从那里继续。所以生成器函数只会在下一次出现yield时再次停止。

所以看看这样的循环:

for _ in range(3):
    score = yield score + 1

第一次使用send 时,上面的代码已经执行了yield score + 1,现在将返回发送的值,并将其分配给scorefor 循环继续并获取range(3) 中的下一个值,开始另一个迭代,然后再次执行yield score + 1 并在该点暂停。然后生成下一个迭代值

现在,如果您想将发送与普通的 next() 迭代结合起来,您可以添加额外的 yield 表达式,但需要定位这些表达式,以便您的代码在右侧暂停地点;当您要调用next() 时,在一个普通的yield value 上(因为它会返回None),在您使用generator.send() 时在target = yield 上(因为它会返回发送的值):

@consumer
def coroutine():
    score = 0
    yield  # 1
    for _ in range(3):
        score = yield score + 1  # 2
        yield  # 3

当您使用上面带有 for 循环的“@consumer”修饰生成器时,会发生以下情况:

  • @consumer 装饰器通过移动到点 1 来“启动”生成器。
  • for 循环在生成器上调用 next(),并前进到第 2 点,生成 score + 1 值。
  • generator.send() 调用在点 2 处返回暂停的生成器,将发送的值分配给 score,并将生成器前进到点 3。这返回 None 作为 generator.send() 结果!
  • for 循环再次调用 next(),前进到第 2 点,为循环提供下一个 score + 1 值。
  • 等等。

所以上面的代码直接与你的循环一起工作:

>>> @consumer
... def coroutine():
...     score = 0
...     yield  # 1
...     for _ in range(3):
...         score = yield score + 1  # 2
...         yield  # 3
...
>>> cs = coroutine()
>>> for c in cs:
...     print(c)
...     cs.send(c + 1)
...
1
3
5

注意@consumer 装饰器和第一个yield 现在可以再次使用; for 循环可以自行推进到第 2 点:

def coroutine():
    score = 0
    for _ in range(3):
        score = yield score + 1  # 2, for advances to here
        yield  # 3, send advances to here

这仍然继续与您的循环一起工作:

>>> def coroutine():
...     score = 0
...     for _ in range(3):
...         score = yield score + 1  # 2, for advances to here
...         yield  # 3, send advances to here
...
>>> cs = coroutine()
>>> for c in cs:
...     print(c)
...     cs.send(c + 1)
...
1
3
5

【讨论】:

  • 值得注意的是,functools.wraps 会为你做双下作业。
  • 谢谢!那么为什么我不能在协程中只使用两个yield 语句——一个用于for 循环,一个用于send?我在我的问题中添加了另一个示例。
  • @z0r:对不起,我昨晚没时间了。我今天早上添加了更多信息。
【解决方案2】:

我会在你的第二次尝试时试一试。首先,让coroutine定义为:

def coroutine():
    score = 0
    for _ in range(3):
        yield
        score = yield score + 1

这个函数会像原来的问题一样输出你的1, 3, 5

现在,让我们将for 循环转换为while 循环。

# for loop
for c in cs:
    print(c)
    cs.send(c + 1)

# while loop
while True:
    try:
        c = cs.send(None)
        print(c)
        cs.send(c + 1)
    except StopIteration:
        break

现在,如果我们在它前面加上一个next(cs),我们可以使用下面的代码让这个while 循环工作。总计:

cs = coroutine()
next(cs)
while True:
    try:
        c = cs.send(None)
        print(c)
        cs.send(c + 1)
    except StopIteration:
        break
# Output: 1, 3, 5

当我们尝试将其转换回 for 循环时,我们有相对简单的代码:

cs = coroutine()
next(cs)
for c in cs:
    print(c)
    cs.send(c + 1)

这会根据需要输出1, 3, 5。问题是在for 循环的最后一次迭代中,cs 已经用尽,但send 再次被调用。那么,我们如何从生成器中获得另一个yield?让我们在末尾添加一个...

def coroutine():
    score = 0
    for _ in range(3):
        yield
        score = yield score + 1
    yield

cs = coroutine()
next(cs)
for c in cs:
    print(c)
    cs.send(c + 1)
# Output: 1, 3, 5

最后一个示例按预期进行迭代,没有StopIteration 异常。

现在,如果我们退后一步,这一切都可以写成:

def coroutine():
    score = 0
    for _ in range(3):
        score = yield score + 1
        yield # the only difference from your first attempt

cs = coroutine()
for c in cs:
    print(c)
    cs.send(c + 1)
# Output: 1, 3, 5

注意yield 是如何移动的,next(cs) 是如何被删除的。

【讨论】:

  • 非常有趣!您将for 循环转换为while 循环令人大开眼界。
  • @z0r,查看最后的更新,将yield 移到最后让一切都变得更干净了。
  • 是的,有趣的是yield 语句是如何绕错方向的!我仍然需要解决这个问题。
猜你喜欢
  • 2023-04-09
  • 2010-10-21
  • 2018-10-14
  • 1970-01-01
  • 1970-01-01
  • 2012-12-16
  • 2020-12-24
  • 2013-11-17
  • 1970-01-01
相关资源
最近更新 更多