Real Programs
Iterators & Generators
How for loops really work, the iterator protocol, writing generators with yield, lazy evaluation and infinite sequences, and generator pipelines.
How a for loop works
for x in collection calls iter(collection) to get an iterator, then calls next() on it repeatedly until it raises StopIteration:
nums = [10, 20, 30]
it = iter(nums)
print(next(it), next(it), next(it))
try:
next(it)
except StopIteration:
print("exhausted")10 20 30 exhausted
An iterable is anything you can loop over (lists, strings, dicts, files, ranges). An iterator is the object that actually remembers the position.
Writing an iterator class
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
print(list(Countdown(5)))
for n in Countdown(3):
print(n, end=" ")
print("liftoff!")[5, 4, 3, 2, 1, 0] 3 2 1 0 liftoff!
Generators: the easy way
A generator function uses yield instead of return. Each yield hands out one value and pauses the function, keeping all its local variables until the next value is requested:
def countdown(start):
while start >= 0:
yield start
start -= 1
def fibonacci():
a, b = 0, 1
while True: # infinite — fine, values are produced only on demand
yield a
a, b = b, a + b
print(list(countdown(5)))
fib = fibonacci()
print([next(fib) for _ in range(10)])
gen = countdown(2)
print(next(gen), next(gen), next(gen))[5, 4, 3, 2, 1, 0] [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 2 1 0
Why generators matter: lazy evaluation
A generator produces values one at a time, so it uses almost no memory no matter how many values it yields:
import sys
squares_list = [n * n for n in range(1_000_000)]
squares_gen = (n * n for n in range(1_000_000))
print("list:", sys.getsizeof(squares_list), "bytes")
print("generator:", sys.getsizeof(squares_gen), "bytes")
print(sum(squares_gen))list: 8448728 bytes generator: 208 bytes 333332833333500000
Generator pipelines
Chain generators like assembly-line stages. Each item flows through all stages before the next item starts:
def read_lines(text):
for line in text.splitlines():
yield line
def non_empty(lines):
for line in lines:
if line.strip():
yield line.strip()
def parse(lines):
for line in lines:
name, score = line.split(",")
yield name, int(score)
data = """Asha,88
Ravi,95
Meera,79
"""
results = parse(non_empty(read_lines(data)))
for name, score in results:
print(f"{name}: {score}")Asha: 88 Ravi: 95 Meera: 79
itertools
import itertools
print(list(itertools.islice(itertools.count(1, 3), 5))) # 1, 4, 7, 10, 13
print(list(itertools.islice(itertools.cycle("AB"), 5)))
print(list(itertools.chain([1, 2], [3], [4, 5])))
print([(k, len(list(g))) for k, g in itertools.groupby("aaabccdddd")])
print(list(itertools.product("xy", [1, 2])))[1, 4, 7, 10, 13]
['A', 'B', 'A', 'B', 'A']
[1, 2, 3, 4, 5]
[('a', 3), ('b', 1), ('c', 2), ('d', 4)]
[('x', 1), ('x', 2), ('y', 1), ('y', 2)]| Iterator class | Generator function | Generator expression | |
|---|---|---|---|
| Syntax | __iter__ + __next__ |
def … yield |
(x for x in xs) |
| Length | most code | short | one line |
| State | kept in attributes | kept automatically | kept automatically |
Test yourself
4 questions · pick an answer to check it instantly
- 1
What does
yielddo?A generator resumes right after theyieldon the nextnext()call. - 2
What exception signals that an iterator has no more values?
forloops catchStopIterationautomatically. - 3
What is printed?
Pythondef g(): yield 1 yield 2 it = g() print(next(it) + next(it))The twonextcalls produce 1 and 2. - 4
Why is
(x * x for x in range(10**8))safe to create while the list version may not be?A generator expression is lazy; the list would need gigabytes.