ch7s3_GeneratorsAndIterators
Generators and iterators form the foundation of **lazy evaluation** in Python.
Chapter 7: Functional Programming
Sub-Chapter: Generators and Iterators โ Lazy and Efficient Data Processing
Generators and iterators form the foundation of lazy evaluation in Python.
They allow you to handle sequences of data one element at a time, making your programs memory-efficient, stream-friendly, and ideal for large datasets or infinite streams.
๐งฉ 1. Iterables vs. Iterators
Iterable
An iterable is any Python object that can return an iterator โ for example, lists, tuples, strings, sets, and dictionaries.
nums = [1, 2, 3]
it = iter(nums)
print(next(it)) # 1
print(next(it)) # 2
Iterator
An iterator is an object with two methods:
__iter__()โ returns itself (the iterator object)__next__()โ returns the next value or raisesStopIterationwhen done
Example of a custom iterator:
class Counter:
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current > self.end:
raise StopIteration
self.current += 1
return self.current - 1
for num in Counter(1, 5):
print(num) # 1, 2, 3, 4, 5
โ Every generator is an iterator, but not every iterator is a generator.
โ๏ธ 2. The Iterator Protocol
| Method | Purpose |
|---|---|
__iter__() | Returns the iterator object |
__next__() | Returns the next item or raises StopIteration |
iter(obj) | Returns an iterator for an iterable |
next(iterator) | Fetches the next item manually |
๐ง 3. Generators โ Functions that Remember State
A generator is a special kind of iterator created with a yield statement inside a function.
def countdown(n):
while n > 0:
yield n
n -= 1
for num in countdown(3):
print(num)
# Output: 3, 2, 1
Each time the generator yields, Python suspends its state until the next call.
โก 4. Lazy Evaluation in Action
Generators process data on demand โ no values are computed until requested.
def infinite_sequence():
num = 0
while True:
yield num
num += 1
gen = infinite_sequence()
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 2
โ ๏ธ Infinite generators must be consumed carefully to avoid endless loops.
๐ 5. Generator Expressions โ Memory-Efficient Alternatives
Generator expressions are compact generators defined inline using parentheses.
squares = (x ** 2 for x in range(10))
print(sum(squares)) # 285
Comparison:
| Comprehension | Type | Memory Usage |
|---|---|---|
[x ** 2 for x in range(10)] | List | Creates full list in memory |
(x ** 2 for x in range(10)) | Generator | Produces values lazily |
๐งฑ 6. yield from โ Delegating to Subgenerators
You can delegate iteration to another generator using yield from, simplifying nested loops.
def generator_a():
yield from range(3)
yield from ["a", "b", "c"]
for value in generator_a():
print(value)
Output:
0
1
2
a
b
c
๐งฎ 7. Advanced Generator Operations
Sending Values into a Generator
You can use .send() to send data back into a running generator.
def echo():
while True:
value = yield
print("Received:", value)
g = echo()
next(g) # Prime the generator
g.send("Hello") # Received: Hello
g.send("World") # Received: World
Closing a Generator
def endless():
while True:
yield "Running..."
g = endless()
print(next(g))
g.close() # Gracefully stops the generator
๐ 8. Itertools โ Power Tools for Iteration
The itertools module enhances generators with combinatorial and infinite tools.
import itertools
count = itertools.count(10, 2) # 10, 12, 14, ...
cycled = itertools.cycle(["A", "B"]) # A, B, A, B, ...
limited = itertools.islice(count, 5) # Take first 5 values
print(list(limited)) # [10, 12, 14, 16, 18]
Other useful itertools functions:
chain(): link multiple iterablescombinations()andpermutations()accumulate(): running totals
๐งพ 9. Real-World Examples
Example 1 โ Fibonacci Sequence
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)])
Example 2 โ Lazy File Reader
def read_large_file(path):
with open(path) as file:
for line in file:
yield line.strip()
for line in read_large_file("data.txt"):
print(line)
Example 3 โ Streaming Data Pipeline
def get_data():
for i in range(1, 1000000):
yield i
def process(data):
for n in data:
yield n * 2
def filter_even(data):
for n in data:
if n % 2 == 0:
yield n
# Combine pipeline
stream = get_data()
stream = process(stream)
stream = filter_even(stream)
for val in stream:
if val > 20:
break
print(val)
๐ง 10. Memory and Performance
| Structure | Data Size | Memory | Evaluation |
|---|---|---|---|
| List | Eager | High | Immediate |
| Generator | Lazy | Low | On-demand |
Generators let you process infinite or massive datasets without exhausting RAM.
๐ 11. Asynchronous Generators (Advanced)
Async generators allow asynchronous iteration in concurrent code:
import asyncio
async def async_counter():
for i in range(3):
yield i
await asyncio.sleep(1)
async def main():
async for value in async_counter():
print(value)
asyncio.run(main())
๐งญ 12. Visualization โ Lazy Data Flow
Iterable โ Iterator โ Generator โ Consumer
| | | |
v v v v
[1,2,3] โ __next__() โ yield n โ print()
๐งพ 13. Best Practices
โ
Use generators for large or infinite sequences.
โ
Prefer generator expressions for quick, one-liners.
โ
Combine with itertools for advanced iteration.
โ
Avoid converting large generators to lists.
โ
Use yield from to simplify nested loops.
โ
Always handle StopIteration when manually consuming.
โ
For concurrency, use async generators.
๐ง Summary
| Concept | Description | Example |
|---|---|---|
| Iterator | Object with __iter__() and __next__() | iter([1,2,3]) |
| Generator | Function that yields values lazily | yield n |
| Generator Expression | Inline generator | (x**2 for x in data) |
yield from | Delegates to another iterable | yield from range(5) |
| Async Generator | Yields in async context | async def ... yield |
Mastering generators and iterators gives you full control over how data flows through your programs โ efficiently, lazily, and elegantly.