Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Iterator as boolean statements?

I came across this code:

def myzip(*args):
    iters = map(iter, args)
    while iters:
        res = [next(i) for i in iters]
        yield tuple(res)

I'm unsure as to:

  • Why the list comprehension doesn't need to catch a StopIteration
  • how does while iters work as I've tried:

    x=[1,2]
    x=iter(x)
    if x: 
        print("Still True")
    next(x)
    next(x)
    if x: 
        print("Still True")
    

and it still prints "Still True" in both cases.

The author of the code also said because map returns a "one-shot iterable" in 3.X and "as soon as we’ve run the list comprehension inside the loop once, iters will be exhausted but still True(and res will be []) forever". He suggested to use list(map(iters, args) instead if we're using 3.X.

I'm unsure of how this change actually helps it to work as I thought that even if iterators are at the StopIteration point it still is True (based on what I tried earlier).

Edit:

The author gave this as an example

>>> list(myzip('abc', 'lmnop'))
[('a', 'l'), ('b', 'm'), ('c', 'n')]
like image 546
Dan Avatar asked Aug 19 '17 13:08

Dan


1 Answers

There are several aspects to the question.

python-2.x

The map returns a list and the while iters just makes sure that the code doesn't go into the loop in case there were no *args passed into the function. That's because an empty list is considered False and a not-empty list is considered True.

In case there are no *args it won't enter the loop and implicitly returns which then raises a StopIteration.

In case there is at least one argument the while iters is equivalent to while True and it relies on one of the iterators to raise a StopIteration after being exhausted. That StopIteration doesn't need to be catched (at least before Python 3.7) because you want the myzip to stop if one iterable is exhausted.

python-3.x

In Python 3 map returns a map instance which will always considered True so the while loop is equivalent to while True.

However, there is one problem in python-3.x: After iterating over a map instance once it will be exhausted. In the first iteration (of the while loop) that works as expected, but in the next iteration map will be empty and it will just create an empty list:

>>> it = map(iter, ([1,2,3], [3,4,5]))
>>> [next(sub) for sub in it]
[1, 3]
>>> [next(sub) for sub in it]
[]

There is nothing that could raise a StopIteration anymore so it will go in an infinite loop and return empty tuples forever. That''s also the reason you don't want to enter the while loop if the iters-list is empty!

It could be fixed (as stated) by using:

iters = list(map(iter, args))

general observation

Just a note on how it would make more sense:

def myzip(*args):
    if not args:
        return
    iters = [iter(arg) for arg in args]  # or list(map(iter, args))
    while True:
        res = [next(i) for i in iters]
        yield tuple(res)

If you want the code to be python-3.7 compliant (thanks @Kevin for pointing this out) you explicitly need to catch the StopIterations. For more informations refer to PEP-479:

def myzip(*args):
    if not args:
        return
    iters = [iter(arg) for arg in args]  # or list(map(iter, args))
    while True:
        try:
            res = [next(i) for i in iters]
        except StopIteration:  
            # the StopIteration raised by next has to be catched in python-3.7+
            return
        yield tuple(res)

The latter code also works on python-2.7 and python-3.x < 3.7 but it's only required to catch the StopIterations in python 3.7+

like image 126
MSeifert Avatar answered Sep 18 '22 20:09

MSeifert