将值发送到 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 发送的值,分配给 score
。 for
循环继续并获取 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)
是如何被移除的。
给定一个 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 发送的值,分配给 score
。 for
循环继续并获取 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。此 returnsNone
作为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)
是如何被移除的。