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