# [Python-ideas] Revised**5 PEP on yield-from

Jacob Holm jh at improva.dk
Sun Mar 1 13:30:36 CET 2009

```Hi Arnaud

Arnaud Delobelle wrote:
>> I have now worked out the details, and it is indeed possible to get
>> O(1) for simple cases and amortized O(logN)
>> in general, all with fairly low constants.
>
> I'm sorry if I'm missing something obvious, but there are two things I
> can't work out:
not obvious.

>
> * What you are measuring the time complexity of.
The time for a single 'next', 'send', 'throw' or 'close' call to a
generator or a single "yield from" expression, excluding the time spent
running the user-defined code in it. (Does that make sense?) In other
words, the total overhead of finding the actual user code to run and
handling the return values/exceptions according to the PEP.

> * What N stands for.
>
> I suspect that N is the 'delegation depth': the number of yield-from
> that have to be gone through. I imagine that you are measuring the
> time it takes to get the next element in the generator. These are
> guesses - can you correct me?
N is the total number of suspended generators in the tree(s) of
generators involved in the operation. Remember that it is possible to
have multiple generators yield from the same 'parent' generator. The
'delegation depth' would be the height of that tree.

One interesting thing to note is that all non-contrived examples I have
seen only build simple chains of iterators, and only do that by adding
one at a time in the deepest nested one. This is the best possible case
for my algorithm. If you stick to that, the time per operation is O(1).
If we decided to only allow that, the algorithm can be simplified
significantly.

>
>> I have implemented the tree structure as a python module and added a
>> trampoline-based pure-python implementation of "yield-from" to try it
>> out.
>>
>> It seems that this version beats a normal "for v in it: yield v" when
>> the delegation chains get around 90 generators deep.
>
> Can you give an example?
>
Sure, here is the simple code I used for timing the 'next' call

import itertools, time
from yieldfrom import uses_from, from_ # my module...

@uses_from
def child(it):
yield from_(it)

def child2(it):
for i in it:
yield i

def longchain(N):
it = itertools.count()
for i in xrange(N):
it = child(it) # replace this with child2 to test the current
# "for v in it: yield v" pattern.
it.next() # we are timing the 'next' calls (not the setup of
# the chain) so skip the setup by calling next once.
return it

it = longchain(90)

times = []
for i in xrange(10):
t1 = time.time()
for i in xrange(100000):
it.next()
t2 = time.time()
times.append(t2-t1)
print min(times)

This version takes about the same time whether you use child or child2
to build the chain. However, the version using yield..from takes the
same time no matter how long the chain is, while the time for the "for v
in it: yield v" version is linear in the length of the chain, and so
would lose big-time for longer chains.