Jacques Gaudin Jacques Gaudin - 7 months ago 12
Python Question

ValueError raised before StopIteration with map(next, iterables)

I am writing a loop over 4 list-iterators like this :

it0 = iter(['foo','bar'])
it1 = iter(['bar','foo'])
it2 = iter(['foo','foo'])
it3 = iter(['bar','bar'])

try:
a, b, c, d = map(next, (it0, it1, it2, it3))
while True:
#some operations that can raise ValueError
if a+b+c+d == 'foobar'*2:
a, b, c, d = map(next, (it0, it1, it2, it3))
elif a+b == 'foobar':
c, d = map(next,(it2, it3))
else:
a, b = map(next,(it0, it1))
except StopIteration:
pass


But when I run this code in Python3, I get a
ValueError: not enough values to unpack (expected 2, got 0)
raised before the expected
StopIteration
.

I do not want to change the exception
StopIteration
to
ValueError
in the
except
statement because some operations in the while loop could result in a
ValueError
that I do not want to catch.

How is it that
next
doesn't raise its exception before the assignement ?

EDIT0: In Python2.7,
StopIteration
is raised first.

EDIT1: In the light of Martijn Pieters answer below, using list comprehension directly works :

a, b, c, d = [next(it) for it in (it0, it1, it2, it3)]

Answer

The StopIteration is raised, but the tuple assignment swallows this as it sees the StopIteration as a signal from the map() iterator that it is done producing values:

>>> i0, i1 = iter(['foo']), iter([])
>>> m = map(next, (i0, i1))
>>> next(m)
'foo'
>>> next(m)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> i0, i1 = iter(['foo']), iter([])
>>> m = map(next, (i0, i1))
>>> a, b = m
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 2, got 1)

This is normal behaviour; Python built-ins that expect an iterator as input always use StopIteration as the 'iteration is done' signal, and tuple unpacking must iterate here.

Either convert the map() output to a list first and test the length, use next() on each iterable separately and not use map(), or catch ValueError locally.

Testing the length would have to re-raise StopIteration:

values = list(map(next, (myIter0, myIter1, myIter2, myIter3)))
if len(values) < 4:
    raise StopIteration
a, b, c, d = values

Note that here list() has swallowed the StopIteration exception.

Catching the ValueError just for the map() operation:

try:    
    a, b, c, d = map(next, (myIter0, myIter1, myIter2, myIter3))
except ValueError:
    raise StopIteration

Not using map() at all:

a, b, c, d = next(myIter0), next(myIter1), next(myIter2), next(myIter3)
Comments