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 calls next()
on the generator, and it advances to point 2, producing the score + 1
value.
- a
generator.send()
call returns paused generator at point 2, assigning the sent value to score
, and advances the generator to point 3. This returns None
as the generator.send()
result!
- the
for
loop calls next()
again, advancing to point 2, giving the loop the next score + 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
for
-looping over a coroutine doesn't really make sense. The way you interact with a coroutine is completely different from the normal mode of interaction with an ordinary generator. – user2357112 supports Monicayield score + 1
returnsNone
and assigns that toscore
, so the next iteration attempts to add 1 toNone
. – apex-meme-lord