将值发送到 Python 协程而不处理 StopIteration

Send values to Python coroutine without handling StopIteration

给定一个 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'

是的,对于协同程序,您通常必须先使用 next() 调用来 'prime' 生成器;它会导致生成器函数执行代码直到第一个 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 循环一起工作,你必须了解当你 [=82] 时来自 for 循环的 next() 调用何时进入=]在循环中。当 .send() 恢复生成器时,yield 表达式 return 发送值 ,生成器从那里继续。因此,生成器函数只会在 下一个 出现 yield 时再次停止。

所以看看这样的循环:

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

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

现在,如果你想将发送与普通 next() 迭代结合起来,你 可以 添加额外的 yield 表达式,但这些表达式需要定位这样您的代码就会在正确的位置暂停;当你要调用 next()(因为它会 return None)和 target = yield 当你使用 generator.send()(因为它将 return 发送的值):

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

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

  • @consumer 装饰器 'primes' 移动到点 1 的生成器。
  • for 循环在生成器上调用 next(),它前进到点 2,产生 score + 1 值。
  • a generator.send() 调用 returns 在点 2 暂停的生成器,将发送的值分配给 score,并将生成器推进到点 3。此 returns 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

我会尝试你的第二次尝试。首先,让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) 是如何被移除的。