[Python-Dev] yield back-and-forth?
Nick Coghlan
ncoghlan at gmail.com
Sat Jan 21 05:40:29 CET 2006
Phillip J. Eby wrote:
>> Thoughts?
>
> If we have to have a syntax, "yield from sub_generator()" seems clearer
> than "yieldthrough", and doesn't require a new keyword.
Andrew Koenig suggested the same phrasing last year [1], and I liked it then.
I don't like it any more, though, as I think it is too inflexible, and we have
a better option available (specifically, stealing "continue with an argument"
from PEP 340). The following ramblings (try to) explain my reasoning :)
Guido does raise an interesting point. The introduction of "send" and "throw"
means that the current simple loop approach does not easily allow values to be
passed down to nested generators, nor does not it correctly terminate nested
generators in response to an invocation of "throw". Because of the explicit
for loop, the nested generator only gets cleaned up in response to GC - it
never sees the exception that occurs in the body of the for loop (at the point
of the yield expression).
The "yield from iterable" concept could be translated roughly as follows:
itr = iter(iterable)
try:
send_input = itr.send # Can input values be passed down?
except AttributeError:
send_input = None
try:
next = itr.next() # Get the first output
except StopIteration:
pass
else:
while 1:
try:
input = yield next # yield and get input
except:
try:
throw_exc = itr.throw # Can exception be passed down?
except AttributeError:
raise # Nope, reraise
else:
throw_exc(sys.exc_info()) # Yep, pass it down
else:
try:
if send_input is None:
if input is not None:
raise TypeError("Cannot send input!")
next = itr.next()
else:
next = send_input(input) # Pass input down
except StopIteration:
break
I'm not particularly happy with this, though, as not only is it horribly
implicit and magical, it's trivial to accidentally break the chain - consider
what happens if you naively do:
yield from (x*x for x in sub_generator())
The chain has been broken - the sub generator no longer sees either passed in
values or thrown exceptions, as the generator expression intercepts them
without passing them down. Even worse, IMO, is that the syntax is entirely
inflexible - we have no easy way to manipulate either the results sent from
the generator, or the input values passed to it.
However, an idea from Guido's PEP 340 helps with the "send" part of the story,
involving passing an argument to continue:
def main_generator():
...
for value in sub_generator():
continue yield value
Here, sub_generator's "send" method would be invoked with the result of the
call to yield value. Manipulation in either direction (input or output) is
trivial:
def main_generator():
...
for value in sub_generator():
input = yield value*value # Square the output values
continue input*input # Square the input values, too
You could even do odd things like yield each value twice, and then pass down
pairs of inputs:
def main_generator():
...
for value in sub_generator():
continue (yield value), (yield value)
The need to use a "continue" statement eliminates the temptation to use a
generator expression, and makes it far less likely the downwards connection
between the main generator and the sub generator will be accidentally broken.
Exception propagation is a different story. What do you want to propagate? All
exceptions from the body of the for loop? Or just those from the yield statement?
Well, isn't factoring out exception processing part of what PEP 343 is for?
# Simply make sure the generator is closed promptly
def main_generator():
...
with closing(sub_generator()) as gen:
for value in gen:
continue yield value
# Or throw the real exception to the nested generator
class throw_to(object):
def __init__(self, gen):
self.gen = gen
def __enter__(self):
return self.gen
def __exit__(self, exc_type, *exc_details):
if exc_type is not None:
try:
self.gen.throw(exc_type, *exc_details)
except StopIteration:
pass
def main_generator():
...
with throw_to(sub_generator()) as gen:
for value in gen:
continue yield value
# We can even limit the propagated exceptions to those
# from the outside world and leave the rest alone
def main_generator():
...
gen = sub_generator()
for value in gen:
with throw_to(gen):
input = yield value
continue input
Thanks to Jason's other thread, I even have a hypothetical use case for all
this. Specifically, "generator decorators", that manipulate the state that
holds while executing the generator. Suppose I have a number of generators
that I want to work in high precision decimal, but yield results with normal
precision. Doing this dance manually in every generator would be a pain, so
let's use a decorator:
def precise_gen(gen_func):
def wrapper(*args, **kwds):
orig_ctx = decimal.getcontext()
with orig_ctx as ctx:
ctx.prec = HIGH_PRECISION
gen = gen_func(*args, **kwds)
for val in gen:
with orig_ctx:
val = +val
try:
yield val
except:
gen.throw() # make it default to sys.exc_info()
wrapper.__name__ = gen_func.__name__
wrapper.__dict__ = gen_func.__dict__
wrapper.__doc__ = gen_func.__doc__
return wrapper
So far, so good (although currently, unlike a raise statement, gen.throw()
doesn't automatically default to sys.exc_info()). Tough luck, however, if you
want to use this on a generator that accepts input values - those inputs will
get dropped on the floor by the wrapper, and the wrapped generator will never
see them.
"yield from" wouldn't help in the least, because there are other things going
on in the for loop containing the yield statement. 'continue with argument',
however, would work just fine:
def precise_gen(gen_func):
def wrapper(*args, **kwds):
orig_ctx = decimal.getcontext()
with orig_ctx as ctx:
ctx.prec = HIGH_PRECISION
gen = gen_func(*args, **kwds)
for val in gen:
with orig_ctx:
val = +val
try:
continue yield val
except:
gen.throw() # make it default to sys.exc_info()
wrapper.__name__ = gen_func.__name__
wrapper.__dict__ = gen_func.__dict__
wrapper.__doc__ = gen_func.__doc__
return wrapper
Now an arbitrary generator can be decorated with "@precise_gen", and it's
internal operations will be carried out with high precision, while any interim
results will be yielded using the caller's precision and the caller's rounding
rules.
Cheers,
Nick.
[1] http://mail.python.org/pipermail/python-dev/2005-October/057410.html
--
Nick Coghlan | ncoghlan at gmail.com | Brisbane, Australia
---------------------------------------------------------------
http://www.boredomandlaziness.org
More information about the Python-Dev
mailing list