[Python-ideas] A cute Python implementation of itertools.tee

Tim Peters tim.peters at gmail.com
Sun Apr 15 12:52:49 EDT 2018


[Antoine Pitrou <solipsis at pitrou.net>]
> This implementation doesn't work with Python 3.7 or 3.8.
> I've tried it here:
> https://gist.github.com/pitrou/b3991f638300edb6d06b5be23a4c66d6
>
> and get:
> Traceback (most recent call last):
>   File "mytee.py", line 14, in gen
>     mylast = last[1] = last = [next(it), None]
> StopIteration
>
> The above exception was the direct cause of the following exception:
>
> Traceback (most recent call last):
>   File "mytee.py", line 47, in <module>
>     run(mytee1)
>   File "mytee.py", line 36, in run
>     lists[i].append(next(iters[i]))
> RuntimeError: generator raised StopIteration
>
> (Yuck!)

Thanks for trying!  I wonder whether that will break other code.  I
wrote PEP 255, and this part was intentional at the time:

"""
If an unhandled exception-- including, but not limited to,
StopIteration --is raised by, OR PASSES THROUGH [emphasis added], a
generator function, then the exception is passed on to the caller in
the usual way, and subsequent attempts to resume the generator
function raise StopIteration.
"""

I've exploited that a number of times.


> In short, you want the following instead:
>
>                 try:
>                     mylast = last[1] = last = [next(it), None]
>                 except StopIteration:
>                     return

No, I don't ;-)  If I have to catch StopIteration myself now, then I
want the entire "white True:" loop in the "try" block.  Setting up
try/except machinery anew on each iteration would add significant
overhead; doing it just once per derived generator wouldn't.


>>     def mytee(xs, n):
>>         last = [None, None]
>>
>>         def gen(it, mylast):
>>             nonlocal last
>>             while True:
>>                 mylast = mylast[1]
>>                 if not mylast:
>>                     mylast = last[1] = last = [next(it), None]

> That's smart and obscure :-o
> The way it works is that the `last` assignment changes the `last` value
> seen by all derived generators, while the `last[1]` assignment updates
> the bindings made in the other generators' `mylast` lists...  It's
> difficult to find the words to explain it.

Which is why I didn't even try - I did warn people that if they
thought it "was obvious", they hadn't yet thought hard enough ;-)
Good job!


> The chained assignment makes it more difficult to parse as well (when I
> read this I don't know if `last[i]` or `last` gets assigned first;
> apparently the answer is `last[i]`, otherwise the recipe wouldn't work
> correctly).

Ya, I had to look it up too :-)  Although, like almost everything else
in Python, chained assignments proceed "left to right".  I was just
trying to make it as short as possible, to increase the "huh - can
something that tiny really work?!" healthy skepticism factor :-)


>  Perhaps like this:
>
>         while True:
>             mylast = mylast[1]
>             if not mylast:
>                 try:
>                     # Create new list link
>                     mylast = [next(it), None]
>                 except StopIteration:
>                     return
>                 else:
>                     # Append to other generators `mylast` linked lists
>                     last[1] = mylast
>                     # Update shared list link
>                     last = last[1]
>             yield mylast[0]

I certainly agree that's easier to follow.  But that wasn't really the point ;-)


More information about the Python-ideas mailing list