在不处理StopIteration的情况下将值发送到Python协同程序

时间:2016-02-17 22:52:33

标签: python generator coroutine

鉴于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'

2 个答案:

答案 0 :(得分:3)

是的,使用协同程序,您通常必须首先使用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循环一起使用,则必须了解当您 in <时,来自next()循环的for调用何时进入/ em>循环。当.send()恢复生成器时,yield表达式返回发送的值,然后生成器从那里继续。因此,生成器函数只会再次停止下一个时间yield出现。

所以看一下这样的循环:

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

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

现在,如果要将发送与普通next()迭代相结合,可以添加额外的yield表达式,但是那些表达式需要定位,以便您的代码是暂停在正确的位置;当您打算使用yield value时,next()打开None(因为它会返回target = yield)和generator.send()时({1}}它会返回发送的值):

@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

答案 1 :(得分: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

现在,如果我们在while之前添加next(cs)循环,我们可以使用以下代码。总计:

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)已移除。