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?
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 ofwhile
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 yield
ed. That's why in the first list yield
ed 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
>>>
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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With