[Python-Dev] PEP 380 (yield from a subgenerator) comments

P.J. Eby pje at telecommunity.com
Fri Mar 27 18:04:18 CET 2009


At 03:28 AM 3/27/2009 -0400, Scott Dial wrote:
>P.J. Eby wrote:
> > One remaining quirk or missing piece: ISTM there needs to be a way to
> > extract the return value without using a yield-from statement.  I mean,
> > you could write a utility function like:
> >
> >    def unyield(geniter):
> >        try:
> >            while 1: geniter.next()
> >        except GeneratorReturn as v:
> >            return v.value
>
>My first thought was to ask why it was not equivalent to say:
>
>     x = yield g
>     x = yield from g
>
>This would seem like a more obvious lack of parallelism to pick on wrt.
>return values.

Because yield-from means you're "inlining" the generator, such that 
sends go into that generator, rather than into the current generator.


>This unyield() operation seems contrived. Never before have you been
>able to write a generator that returns a value, why would these suddenly
>become common practice? The only place a return value seems useful is
>when refactoring a generator and you need to mend having loss of a
>shared scope. What other use is there for a return value?

The use case which these things are being proposed for is to replace 
most of the stack-management code that's currently needed for 
coroutine trampolines.  In such a case, you're likely using 
generators to perform long-running asynchronous operations, or else 
coroutines where two functions are co-operating to produce a result, 
each with its own control flow.

For example, you might have a generator that yields socket objects to 
wait for them to be ready to read or write, then returns a line of 
text read from the socket.  You would unyield this if you wanted to 
write top-level code that was *not* also such a task.  Similarly, you 
might write coroutines where one reads data from a file and sends it 
to a parser, and then the parser sends data back to a main program.

In either case, an unyield would either be the synchronous top-level 
loop of the program, or part of the top-level code.  Either you need 
to get the finished top-level object from your parser at the end of 
its operation, or you are waiting for all your asynchronous I/O tasks 
to complete.


>It would seem unfortunate for it to be considered a runtime error since
>this would prevent sharing a generator amongst "yield from" and
>non-"yield from" use cases.

Has anyone shown a use case for doing so?  I might be biased due to 
previous experience with these things, but I don't see how you write 
a function where both the yielded values *and* the return value are 
useful...  and if you did, you'd still need some sort of unyield operation.

Notice that in both the I/O and coroutine use cases, the point of 
yielding is primarily *to allow other code to execute*, and possibly 
pass a value back IN to the generator.  The values passed *out* by 
the generator are usually either ignored, an indicator of what the 
generator wants to be passed back in, or what sort of event it is 
waiting for before it's to be resumed.

In other words, they're usually not data -- they're just something 
that gets looped over as the task progresses.


>As Greg has said a number of times, we allow functions to return values
>with them silently being ignored all the time.

Sure.  But right now, the return value of a generator function *is 
the generator*.  And you're free to ignore that, sure.

But this is a "second" return value that only goes to a special place 
with special syntax -- without that syntax, you can't access it.

But in the use cases where you'd actually want to make such a 
function return a value to begin with, it's because that value is the 
value you *really* want from the function -- the only reason it's a 
generator is because it needs to be paused and resumed along the way 
to getting that return value.

If you're writing a function that yields values for other than 
control flow reasons, it's probably a bad idea for it to also have a 
"return" value....  because then you'd need an unyield operation to 
get at the data.

And it seems to me that people are saying, "but that's no problem, 
I'll just use yield-from to get the value".  But that doesn't *work*, 
because it turns the function where you use it into another generator!

The generators have to *stop* somewhere, in order for you to *use* 
their return values -- which makes the return feature ONLY relevant 
to co-routine use cases -- i.e., places where you have trampolines or 
a top-level loop to handle the yields...

And conversely, if you *have* such a generator, its real return value 
is the special return value, so you're not going to be able to use it 
outside the coroutine structure...  so "ignoring its return value" 
doesn't make any sense.  You'd have to write a loop over the 
generator, *just to ignore the value*...  which once again is why 
you'd want an unyield operator of some kind.

That's why special return values should be special: you have to 
handle them differently in order to receive that return value... and 
it's monumentally confusing to look at a function with a normal 
'return' that never actually "returns" that value.

A lot of the emails that have been written about this are failing to 
understand the effects of the control-flow proposed by the 
PEP.   IMO, this should be taken as evidence that using a plain 
"return" statement is in fact confusing, *even to Python-Dev 
participants who have read the PEP*.

We would be much better off with something like "yield return X" or 
"return from yield with X", as it would highlight this 
otherwise-obscure and "magical" difference in control flow.



More information about the Python-Dev mailing list