Send values to Python coroutine without handling StopIteration
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.
Yes, with coroutines you generally have to use a next()
call first to 'prime' the generator; it'll cause the generator function to execute code until the first yield
. Your issue is mostly that you are using a for
loop, however, which uses next()
as well, but doesn't send anything.
You could add an extra yield
to the coroutine to catch that first priming step, and add the @consumer
decorator from PEP 342 (adjusted for Python 2 and 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
You'd still have to use a while
loop, as a for
loop can't send:
c = 0
while True:
try:
c = cs.send(c + 1)
except StopIteration:
break
print(c)
Now, if you want this to work with a for
loop, you have to understand when the next()
call from the for
loop comes in when you are in the loop. When the .send()
resumes the generator, the yield
expression returns the sent value, and the generator continues on from there. So the generator function only stops again the next time a yield
appears.
So looking at a loop like this:
for _ in range(3):
score = yield score + 1
the first time you use send
the above code has already executed yield score + 1
and that'll now return the sent value, assigning it to score
. The for
loop continues on and takes the next value in the range(3)
, starts another iteration, then executes yield score + 1
again and pauses at that point. It is that next iteration value that is then produced.
Now, if you want to combine sending with plain next()
iteration, you can add extra yield
expressions, but those then need to be positioned such that your code is paused in the right locations; at a plain yield value
when you are going to call next()
(because it'll return None
) and at a target = yield
when you are using generator.send()
(because it'll return the sent value):
@consumer
def coroutine():
score = 0
yield # 1
for _ in range(3):
score = yield score + 1 # 2
yield # 3
When you use the above '@consumer' decorated generator with a for
loop, the following happens:
- the
@consumer
decorator 'primes' the generator by moving to point 1. - the
for
loop callsnext()
on the generator, and it advances to point 2, producing thescore + 1
value. - a
generator.send()
call returns paused generator at point 2, assigning the sent value toscore
, and advances the generator to point 3. This returnsNone
as thegenerator.send()
result! - the
for
loop callsnext()
again, advancing to point 2, giving the loop the nextscore + 1
value. - and so on.
So the above works directly with your loop:
>>> @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
Note that the @consumer
decorator and the first yield
can now go again; the for
loop can do that advancing to point 2 all by itself:
def coroutine():
score = 0
for _ in range(3):
score = yield score + 1 # 2, for advances to here
yield # 3, send advances to here
and this still continues to work with your loop:
>>> 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