Iterators & Generators
Generator Functions with yield
Processing a 10GB log file shouldn't require 10GB of memory. Generator functions with yield produce values one at a time on demand, letting you work with massive datasets or infinite sequences using constant memory.
Infinite Generators
basic.py
# Basic generator
def count_up(n):
"""Generate numbers from 0 to n-1"""
i = 0
while i < n:
yield i
i += 1
# Use generator in for loop
limit =
for num in count_up(limit):
print(num, end=" ")
print("\n")
# Manual iteration
gen = count_up(3)
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 2
# Basic generator
def count_up(n):
"""Generate numbers from 0 to n-1"""
i = 0
while i < n:
yield i
i += 1
# Use generator in for loop
limit =
for num in count_up(limit):
print(num, end=" ")
print("\n")
# Manual iteration
gen = count_up(3)
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 2
# Basic generator
def count_up(n):
"""Generate numbers from 0 to n-1"""
i = 0
while i < n:
yield i
i += 1
# Use generator in for loop
limit =
for num in count_up(limit):
print(num, end=" ")
print("\n")
# Manual iteration
gen = count_up(3)
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 2
state.py
# State preservation
def fibonacci(n):
"""Generate first n Fibonacci numbers"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b # state preserved across yields
# State is preserved
print("Fibonacci sequence:")
for num in fibonacci(10):
print(num, end=" ")
lazy_vs_eager.py
# Generator vs list
def eager_range(n):
"""Returns a list (all values in memory)"""
result = []
for i in range(n):
result.append(i)
return result
def lazy_range(n):
"""Returns a generator (values on demand)"""
for i in range(n):
yield i
# Eager: all values created upfront
print("Eager (list):")
nums = eager_range(5)
print(type(nums), nums)
# Lazy: values created on demand
print("\nLazy (generator):")
gen = lazy_range(5)
print(type(gen), gen)
print("Values:", list(gen))
expression.py
# Generator expressions
# Generator expression (lazy)
gen = (x * x for x in range(5))
print("Type:", type(gen))
print("Values:", list(gen))
# Compare with list comprehension (eager)
lst = [x * x for x in range(5)]
print("\nList:", type(lst), lst)
# Pipeline with generator expressions
numbers = range(10)
squares = (x * x for x in numbers)
evens = (x for x in squares if x % 2 == 0)
print("\nEven squares:", list(evens))
infinite.py
# Infinite generators
def infinite_count(start=0):
"""Infinite counter"""
n = start
while True:
yield n
n += 1
# Use with break or takewhile
counter = infinite_count(1)
for num in counter:
print(num, end=" ")
if num >= 10:
break
print("\n")
# With itertools.islice
from itertools import islice
counter = infinite_count(100)
print("First 5:", list(islice(counter, 5)))
yield vs return
returnends the function and returns a valueyieldpauses the function and produces a value, resuming later
Key Benefits
- Lazy evaluation: values produced on demand, not all at once
- State preservation: local variables preserved between yields
- Memory efficient: only one value in memory at a time
- Automatic iterator: no need to write
__iter__and__next__
yield - a keyword that pauses the function and produces a value, resuming execution on the next iteration
lazy evaluation - computing values only when needed, rather than all at once upfront
generator expression - a compact syntax `(expr for x in iterable)` that creates a generator inline
Exercise: practical.py
Build a file reader that yields lines matching a pattern