Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

map function run into infinite loop in 3.X

I'm currently studying iteration in python.

I have encountered the following code.

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

list(myzip('abc', '1mnop'))

When I run the code in 3.X, the code runs into a infinite loop, and prints

['a', '1']
[]
[]
[]
...

The explanation I got from the author is

3.X map returns a one-shot iterable object instead of a list as in 2.X. In 3.X, 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.

But I am still struggling to understand what is happening and why it is happening.

And also, why is variable res only assigned value ('a', 'l') in the first iteration of while loop? Why is it not assigned ('b', 'm'), and then ('c', 'n') in second and third iteration?

like image 781
Thor Avatar asked Mar 07 '18 03:03

Thor


2 Answers

Problem

But I am still struggling to understand what is happening and why it is happening.

And also, why is variable res only assigned value ('a', 'l') in the first iteration of while loop? res is always assigned an empty list [] afterwards. Why is it not assigned ('b', 'm'), and then ('c', 'n') in second and third iteration?

The reason the code you posted works in Python 2 while failing in Python 3, is because the built-in map returns an iterator in Python 3, rather than a list as it did in Python 2.

Of course this doesn't really explain much unless you know what an iterator is. Although I could go in-depth about what an iterator is exactly1, the important part of iterators to understand here, is that: An iterator can only be iterated over once. Once you've iterated over an iterator once, it's exhausted. It's done. You can't use it anymore.2

When you iterate over the iters iterator in the list comprehension in your code, then iters is done and exhausted, and can no longer be used. So essentially all the list comprehension:

[next(i) for i in iters]

does is grab the first item from each iterator in iters (which are 'a' and 'l'), and then store those in a list. On the next iteration of your while loop, iters can no longer be used, its empty. So empty list are yielded. That's why in the first list yielded you see 'a' and 'l', while other subsequent list are empty.

Lastly, the reason you're code degrades into an infinite loop, is because of the fact that an iterator object - even one that's been exhausted - will evaluate to True in a boolean context:

>>> it = map(str, [1, 2])
>>> next(it)
'1'
>>> next(it)
'2'
>>> # The `it` iterator is exhausted
>>> next(it)
Traceback (most recent call last):
  File "<pyshell#17>", line 1, in <module>
    next(it)
StopIteration
>>> bool(it) # but it still evaluates to `True` in a boolean context
True
>>> 

Solution

The simplest solution to this problem is to cast the iterator returned by map into a list, since list objects support being iterated over multiple times:

>>> def custom_zip(*args):
    iters = list(map(iter, args))
    while iters:
        yield tuple([next(it) for it in iters])


>>> list(custom_zip('abc', [1, 2, 3]))
[('a', 1), ('b', 2), ('c', 3)]
>>> list(custom_zip('def', [4, 5, 6]))
[('d', 4), ('e', 5), ('f', 6)]
>>> list(custom_zip([1, 2, 3], [1, 4, 9], [1, 8, 27]))
[(1, 1, 1), (2, 4, 8), (3, 9, 27)]
>>> 

As @Chris_Rands also noted, although the above code works, a more idiomatic way to implement a custom zip function in Python 3+ would be:

def custom_zip(*args):
    return map(lambda *x: x, *args)

1As a side note, if you would like to understand what an iterator is in-depth, see the question What exactly are Python's iterator, iterable, and iteration protocols?

2For a more complete look into why exhausted iterators evaluate to True, see the question How can I get generators/iterators to evaluate as False when exhausted?

like image 187
Christian Dean Avatar answered Sep 27 '22 21:09

Christian Dean


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

print (list(myzip('abc', '1mnop','yada')))

Output

['a', '1', 'y']
['b', 'm', 'a']
['c', 'n', 'd']
[('a', '1', 'y'), ('b', 'm', 'a'), ('c', 'n', 'd')]

Reason as provided by Christian Dean.

like image 23
Night Shade Avatar answered Sep 27 '22 21:09

Night Shade