While working on a learning task involving overlapping n-wise windows from an input iterable - similar to what itertools.pairwise does - I came across code like this:
def f(seq):
it = iter(seq)
for x in it: break
for y in it:
yield x, y
x = y
The line for x in it: break
was used instead of x = next(it, None)
to avoid assigning x
at all if seq
is empty, which would be the case with the next()
call.
I'm not familiar with CPython internals or low-level details, so I can't say whether this is a justified choice. Could someone help me understand the pros and cons of both approaches for advancing an iterator by one item before a loop?
next()
is more readable, and I think inarguably so. But the for
trick generally runs faster, and that's primarily what drives most uses I've seen. Although speed wouldn't matter in the "one-and-done" context you're showing.
The line for x in it: break was used instead of x = next(it, None) to avoid assigning x at all if seq is empty,
It's more because x = next(it)
would raise the StopIteration
exception, and the coder didn't want to bother with try/except
to deal with that. Note that x
is assigned to in all cases when x = next(it, None)
is used. If the iterator is exhausted, next(it, None)
returns the specified default (None
), and x
is bound to that None
.
A slight extension is also common:
for x in it:
break
else:
# No break, so StopIteration must have been raised
That variation is arguably more readable than:
if (x := next(it, None)) is None:
# StopIteration must have been raised
and then you have to worry too about whether a non-empty iterator may yield None
.
try:
x = next(it)
except StopIteration:
pass
is straightforward and clear, but somehow "feels like" overkill. It's wordy. Which is why next()
has an optional argument to specify a default.
Bottom line: use whichever you like best :-) But you'll see all ways of doing this in other code, and should learn to live with them. Nobody is going to change what they like best ;-)