Reputation: 133
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:
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')]
Upvotes: 6
Views: 2801
Reputation: 152765
There are several aspects to the question.
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 return
s 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.
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 tuple
s 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))
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 StopIteration
s. 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 StopIteration
s in python 3.7+
Upvotes: 7
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