The iterator protocol: why for calls iter() exactly once
An iterable implements __iter__ (returns an iterator). An iterator implements both __iter__
(returning itself) and __next__ (returns the next value or raises StopIteration). for x in obj desugars
to roughly: it = iter(obj); while True: try: x = next(it) except StopIteration: break. Understanding this
is what lets you read (and write) any custom iterable class in a codebase without treating it as magic.
class CountUp:
"""Iterable: a fresh iterator each time -> can be iterated multiple times independently."""
def __init__(self, n):
self.n = n
def __iter__(self):
return CountUpIterator(self.n)
class CountUpIterator:
def __init__(self, n):
self.n = n
self.i = 0
def __iter__(self):
return self
def __next__(self):
if self.i >= self.n:
raise StopIteration
self.i += 1
return self.i
c = CountUp(3)
print(list(c), list(c)) # both calls produce a fresh iterator -> both full runs
it = iter(c)
print(next(it), next(it), next(it))
try:
next(it)
except StopIteration:
print("StopIteration after exhaustion, as expected")
A plain iterator object (not a fresh-iterable factory) gets consumed: iterating it twice the second time yields nothing, which is a common source of 'why is my data empty the second time I loop over it' bugs.
numbers = iter([1, 2, 3])
print("first pass: ", list(numbers))
print("second pass:", list(numbers), " <- already exhausted, iterator has no reset")