Dan
Dan

Reputation: 133

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:

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')]

Upvotes: 6

Views: 2801

Answers (2)

MSeifert
MSeifert

Reputation: 152765

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+

Upvotes: 7

Martijn Pieters
Martijn Pieters

Reputation: 1124100

Why the list comprehension doesn't need to catch a StopIteration

The whole point of this loop is for next() to raise a StopIteration and the list comprehension not to catch it. It's how this generator exits; the while loop is essentially endless, the condition it tests is stable.

That way the generator stops neatly when one of the inputs has been exhausted:

>>> g = myzip([1], [2, 3])
>>> next(g)
(1, 2)
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in myzip
StopIteration

The StopIteration exception was raised by the next() call on the first iterator in iters.

In essence, all that while iters: does is return False if you called myzip() with no inputs at all:

>>> list(myzip())
[]

Otherwise, it may as well use while True: for all the difference it makes.

Now, in Python 3, map(iter, args) returns an iterator, not a list. That object can only be iterated over once, but it is not considered empty and will always have a boolean value of true. That means that the second run through the while iters: loop, iters is true, but [next(i) for i in iters] iterates 0 times, next() is never called and the required StopIteration to exit the generator is never raised. That's why the author suggested list(map(iter, args)) as a work-around, to capture the map iterator values into a list.

Upvotes: 4

Related Questions