Given a Python coroutine:
def coroutine():
score = 0
for _ in range(3):
score = yield score + 1
I'd like to use it in a simple loop like this:
cs = coroutine()
for c in cs:
print(c)
cs.send(c + 1)
... which I would expect to print
1
3
5
But actually, I get an exception on the line yield score + 1
:
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
I can get it to work if I call next
manually:
c = next(cs)
while True:
print(c)
try:
c = cs.send(c + 1)
except StopIteration:
break
But I don't like that I need to use try/except, given that generators are usually so elegant.
So, is there any way to use a finite coroutine like this without explicitly handling StopIteration
? I'm happy to change both the generator and the way I'm iterating over it.
Martijn points out that both the for
loop and my call to send
advance the iterator. Fair enough. Why, then, can't I get around that with two yield statements in the coroutine's loop?
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)
If I try that, I get the same error but on the send
line.
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'
I'll take a stab at your second attempt. First, let coroutine
be defined as:
def coroutine():
score = 0
for _ in range(3):
yield
score = yield score + 1
This function will output your 1, 3, 5
as in the original question.
Now, let's convert the for
loop into a while
loop.
# 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
Now, we can get this while
loop working using the following if we precede it with a next(cs)
. In total:
cs = coroutine()
next(cs)
while True:
try:
c = cs.send(None)
print(c)
cs.send(c + 1)
except StopIteration:
break
# Output: 1, 3, 5
When we try to convert this back into a for loop, we have the relatively simple code:
cs = coroutine()
next(cs)
for c in cs:
print(c)
cs.send(c + 1)
And this outputs the 1, 3, 5
as you wanted. The issue is that in the last iteration of the for
loop, cs
is already exhausted, but send
is called again. So, how do we get another yield
out of the generator? Let's add one to the end...
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
This final example iterates as intended without a StopIteration
exception.
Now, if we take a step back, this can all be better written as:
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
Notice how the yield
moved, and the next(cs)
was removed.