[Python-ideas] PEP on yield-from: throw example
Bruce Frederiksen
dangyogi at gmail.com
Sun Feb 15 20:41:42 CET 2009
Steven D'Aprano wrote:
> If I've understood the protoPEP, it wraps four distinct pieces of
> functionality:
>
> "yield from" pass-through
> pass-through for send
> pass-through for throw
> pass-through for close
>
> I think each one needs to be justified, or at least explained,
> individually. I'm afraid I'm not even clear on what pass-through for
> send/throw/close would even mean, let alone why they would be useful.
> Basic yield pass-through is obvious, and even if we decide that it's
> nothing more than syntactic sugar for "for x in gen: yield x", I think
> it's a clear win for readability. But the rest needs some clear,
> simple examples of how they would be used.
OK, let's give this a try. I to do several posts, one on each item
above in an attempt to demonstrate what we're talking about here.
First of all, to be clear on this, the send, throw and close mechanisms
were proposed in PEP 342 and adopted in Python 2.5. For some reason
though, these new mechanisms didn't seem to make it into the standard
Python documentation. So you'll need to read PEP 342 if you have any
question on how these work.
This post is on "pass-through for close".
I've tried to make these as simple as possible, but there's still a
little bit to it, so please bear with me.
Let's get started.
We're going to do a little loan application program. We're going to
process a list of loan applications. Each loan application consists of
a list of people. If any of the people on the list qualify, then they
get the loan. If none of the people qualify, they don't get the loan.
We're going to have a generator that generates the individual names. If
the name does not qualify, then DoesntQualify is raised by the caller
using the throw method:
class DoesntQualify(Exception): pass
Names = [['Raymond'], ['Bruce', 'Marilyn'], ['Jack', 'Jill']]
def gen(l):
count = 0
try:
for names in l:
count += 1
for name in names:
try:
yield name
break
except DoesntQualify:
pass
else:
print names, "don't qualify"
finally:
print "processed", count, "applications"
Now we need a function that gets passed this generator and checks each
name to see if it qualifies. I would expect to be able to write:
def process(generator):
for name in generator:
if len(name) > 5:
print name, "qualifies"
else:
raise DoesntQualify
But running this gives:
>>> g = gen(Names)
>>> process(g)
Raymond qualifies
Traceback (most recent call last):
File "throw2.py", line 34, in <module>
process(g)
File "throw2.py", line 31, in process
raise DoesntQualify
__main__.DoesntQualify
What I expected was the for statement in process would forward the
DoesntQualify exception to the generator. But it doesn't do this, so
I'm left to do it myself. My next try developing this example, was:
def process(generator):
for name in generator:
while True:
if len(name) > 5:
print name, "qualifies"
break
else:
name = generator.throw(DoesntQualify)
But running this gives:
Raymond qualifies
Marilyn qualifies
['Jack', 'Jill'] don't qualify
processed 3 applications
Traceback (most recent call last):
File "throw2.py", line 46, in <module>
process2(gen(Names))
File "throw2.py", line 43, in process2
name = iterable.throw(DoesntQualify)
StopIteration
Oops, the final throw raised StopIteration when it hit the end of
Names. So I end up with:
def process(generator):
try:
for name in generator:
while True:
if len(name) > 5:
print name, "qualifies"
break
else:
name = generator.throw(DoesntQualify)
except StopIteration:
pass
This one works:
Raymond qualifies
Marilyn qualifies
['Jack', 'Jill'] don't qualify
processed 3 applications
But by this time, it's probably more clear if I just abandon the for
statement entirely:
def process(generator):
name = generator.next()
while True:
try:
if len(name) > 5:
print name, "qualifies"
name = generator.next()
else:
name = generator.throw(DoesntQualify)
except StopIteration:
break
But now I need to change process to add a limit to the number of
accepted applications:
def process(generator, limit):
name = generator.next()
count = 1
while count <= limit:
try:
if len(name) > 5:
print name, "qualifies"
name = generator.next()
count += 1
else:
name = generator.throw(DoesntQualify)
except StopIteration:
break
Seems easy enough, except that this is broken again because the final
"processed N applications" message won't come out if the limit is hit
(unless you are running CPython and call it in such a way that the
generator is immediately collected -- but this doesn't work on jython or
ironpython). That's what the close method is for, and I forgot to call it:
def process(generator, limit):
name = generator.next()
count = 1
while count <= limit:
try:
if len(name) > 5:
print name, "qualifies"
name = generator.next()
count += 1
else:
name = generator.throw(DoesntQualify)
except StopIteration:
break
generator.close()
So what starts out conceptually simple, ends up more complicated and
error prone that I had expected; and the reason is that the for
statement doesn't support these new generators methods. If it did, I
would have:
def process(generator, limit):
count = 1
for generator as name: # new syntax doesn't break old code
if len(name) > 5:
print name, "qualifies"
count += 1
if count > limit: break
else:
raise DoesntQualify # new for passes this to generator.throw
# new for remembers to call generator.close for me.
Now, we need to extend this because there are several lists of
applications. I'd like to be able to use the same gen function on each
list, and the same process function and just introduce an intermediate
generator that gathers up the output of several generators. This is
exactly what itertools.chain does! So this should be very easy:
>>> g1 = gen(Names1)
>>> g2 = gen(Names2)
>>> g3 = gen(Names3)
>>> process(itertools.chain(g1, g2, g3), limit=5)
But, nope, itertools.chain doesn't honor the extra generator methods
either. If we had yield from, then I could use that instead of
itertools.chain:
def multi_gen(gen_list):
for gen in gen_list:
yield from gen
When I use yield from, it sets multi_gen aside and lets process talk
directly to each generator. So I would expect that not only would
objects yielded by each generator be passed directly back to process,
but that exceptions passed in by process with throw would be passed
directly to the generator. Why would this *not* be the case? With the
for statement, I can see that doing the throw/close processing might
break some legacy code and understand the reservation in doing so
there. But here we have a new language construct where we don't need to
worry about legacy code. It's also a construct dealing directly and
exclusively with generators.
If I can't use yield from, and itertools.chain does work, and the for
statement doesn't work, then I'm faced once again with having to code
everything again myself:
def multi_gen(gen_list):
for gen in gen_list:
while True:
try:
yield gen.next()
except DoesntQualify, e:
yield gen.throw(e)
except StopIteration:
gen.close()
Yuck! Did I get this one right? Nope, same StopIteration problem with
gen.throw... Let's try:
def multi_gen(gen_list):
for gen in gen_list:
try:
while True:
try:
yield gen.next()
except DoesntQualify, e:
yield gen.throw(e)
except StopIteration:
pass
finally:
gen.close()
Even more yuck! This feels more like programming in assembler than
python :-(
-bruce frederiksen
More information about the Python-ideas
mailing list