[Python-ideas] yield-from and @coroutine decorator [was:x=(yield from) confusion]
Jacob Holm
jh at improva.dk
Fri Apr 3 17:15:16 CEST 2009
Hi Nick,
Your reworking of my "averager" example has highlighted another issue
for me, which I will get to below. First a few comments on your message.
Nick Coghlan wrote:
> [snip]
>
> The following reworking of Jacob's example assumes a couple of things
> that differ from the current PEP:
>
> - the particular colour my bikeshed is painted when it comes to
> returning values from a generator is "return finally" (the idea being to
> emphasise that this represents a special "final" value for the generator
> that happens only after all of the normal yields are done).
>
We should probably drop that particular bikeshed discussion until we
actually know the details of what the construct should do, esp in the
context of close(). I am starting to lose track of all the different
possible versions.
> - rather than trying to change the meaning of GeneratorExit and close(),
> 3 new generator methods would be added: next_return(), send_return() and
> throw_return(). The new methods have the same signatures as their
> existing counterparts, but if the generator raises GeneratorReturn, they
> trap it and return the associated value instead. Like close(), they
> complain with a RuntimeError if the generator doesn't finish. For example:
>
> def throw_return(self, *exc_info):
> try:
> self.throw(*exc_info)
> raise RuntimeError("Generator did not terminate")
> except GeneratorReturn as gr:
> return gr.value
>
I don't much like the idea of adding these methods, but that is not the
point of this mail.
> (Note that I've also removed the 'yield raise' idea from the example -
> if next() or send() triggers termination of the generator with an
> exception other than StopIteration, then that exception is already
> propagated into the calling scope by the existing generator machinery. I
> realise Jacob was trying to make it possible to "yield an exception"
> without terminating the coroutine, but that idea is well beyond the
> scope of the PEP)
>
I think it was pretty clearly marked as out of scope for this PEP, but I
still like the idea.
> You then get:
>
> class CalcAverage(Exception): pass
>
> def averager(start=0):
> # averager that maintains a running average
> # and returns the final average when done
> count = 0
> exc = None
> sum = start
> while 1:
> avg = sum / count
> try:
> val = yield avg
> except CalcAverage:
> return finally avg
> sum += val
> count += 1
>
> avg = averager()
> avg.next() # start coroutine
> avg.send(1.0) # yields 1.0
> avg.send(2.0) # yields 1.5
> print avg.throw_return(CalcAverage) # prints 1.5
>
This version has a bug. It will raise ZeroDivisionError on the initial
next() call used to start the generator. A better version if you insist
on yielding the running average, would be:
def averager(start=0):
# averager that maintains a running average
# and returns the final average when done
count = 0
sum = start
avg = None
while 1:
try:
val = yield avg
except CalcAverage:
return finally avg
sum += val
count += 1
avg = sum/count
> Now, suppose I want to write another toy coroutine that calculates the
> averages of two sequences and then returns the difference:
>
> def average_diff(start=0):
> avg1 = yield from averager(start)
> avg2 = yield from averager(start)
> return finally avg2 - avg1
>
> diff = average_diff()
> diff.next() # start coroutine
> # yields 0.0
> avg.send(1.0) # yields 1.0
> avg.send(2.0) # yields 1.5
> diff.throw(CalcAverage) # Starts calculation of second average
> # yields 0.0
> diff.send(2.0) # yields 2.0
> diff.send(3.0) # yields 2.5
> print diff.throw_return(CalcAverage) # Prints 1.0 (from "2.5 - 1.5")
>
>
(There is another minor bug here: the two avg.send() calls should have
been diff.send()).
Now for my problem. The original averager example was inspired by the
tutorial http://dabeaz.com/coroutines/ that Guido pointed to. (Great
stuff, btw). One pattern that is recommended by the tutorial and used
throughout is to decorate all coroutines with a decorator like:
def coroutine(func):
def start(*args,**kwargs):
cr = func(*args,**kwargs)
cr.next()
return cr
return start
The idea is that it saves you from the initial next() call used to start
the coroutine. The problem is that you cannot use such a decorated
coroutine in any flavor of the yield-from expression we have considered
so far, because the yield-from will start out by doing an *additional*
next call and yield that value.
I have a few vague ideas of how we might change "yield from" to support
this, but nothing concrete enough to put here. Is this a problem we
should try to fix, and if so, how?
not-trying-to-be-difficult-ly yours
- Jacob
More information about the Python-ideas
mailing list