Teacher's Guide
Chapter 1: Generators
Teaching Objectives
By the end of this chapter, students should:
- Understand the concept of generators in Python
- Know how to create and use generator functions
- Understand the advantages of generators over regular functions returning lists
- Master generator expressions and their syntax
- Apply generators to solve real-world programming problems
Preparation
Before teaching this chapter, ensure:
- Students have a solid understanding of functions, loops, and basic Python data structures
- You've prepared examples demonstrating memory usage differences
- You have working examples of real-world applications of generators
Lesson Overview
1. Introduction to Generators (15 minutes)
Start by explaining what generators are and why they're useful:
- Generators are special functions that return an iterator
- They generate items one at a time and only when requested (lazy evaluation)
- They use the
yieldstatement instead ofreturn - They maintain their state between calls, allowing them to resume where they left off
Key benefits:
- Memory efficiency (values are generated as needed, not all at once)
- Simplicity for certain algorithms
- Ability to work with infinite sequences
- Performance improvements for large datasets
2. Creating Generator Functions (25 minutes)
Introduce how to create a generator function:
# Basic generator function
def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1
# Using the generator
counter = count_up_to(5)
print(next(counter)) # 1
print(next(counter)) # 2
print(next(counter)) # 3
print(next(counter)) # 4
print(next(counter)) # 5
# print(next(counter)) # StopIteration error
# Using the generator in a loop
for number in count_up_to(5):
print(number) # Prints 1, 2, 3, 4, 5
Explain the components:
- The
yieldstatement is what makes a function a generator - Each call to
next()executes the function until it reaches ayieldstatement - The function "pauses" at the
yieldand returns the yielded value - On the next call, the function resumes from where it left off
- When the function exits, the generator is exhausted and raises
StopIteration
Demo with state preservation:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Create a generator for Fibonacci numbers
fib = fibonacci()
# Get the first 10 Fibonacci numbers
for _ in range(10):
print(next(fib), end=" ") # 0 1 1 2 3 5 8 13 21 34
3. Generators vs. Lists (20 minutes)
Compare generators to traditional list approaches:
# List approach - all values are computed at once
def get_squares_list(n):
result = []
for i in range(n):
result.append(i ** 2)
return result
# Generator approach - values are computed on demand
def get_squares_generator(n):
for i in range(n):
yield i ** 2
# Memory comparison for large n
import sys
n = 1000000
# Don't run this next line for very large n - it may consume too much memory
squares_list = get_squares_list(n)
squares_gen = get_squares_generator(n)
# Compare memory usage
list_size = sys.getsizeof(squares_list) if 'squares_list' in locals() else "Too large"
gen_size = sys.getsizeof(squares_gen)
print(f"List size: {list_size} bytes")
print(f"Generator size: {gen_size} bytes")
Teaching points:
- Highlight the memory efficiency of generators for large sequences
- Explain that generators compute values on-demand (lazy evaluation)
- Demonstrate the time difference in initialization (generators are instant)
- Show that generators can only be iterated over once
4. Generator Expressions (15 minutes)
Introduce generator expressions as a compact way to create generators:
# List comprehension (creates the entire list at once)
squares_list = [x**2 for x in range(10)]
# Generator expression (creates a generator object)
squares_gen = (x**2 for x in range(10))
print(squares_list) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(squares_gen) # <generator object <genexpr> at 0x...>
# Using the generator expression
for square in squares_gen:
print(square, end=" ") # 0 1 4 9 16 25 36 49 64 81
Teaching approach:
- Compare with list comprehensions (parentheses vs. square brackets)
- Emphasize the syntax similarity but different behavior
- Show examples where generator expressions are more appropriate
5. Practical Use Cases (20 minutes)
Demonstrate real-world applications of generators:
Reading large files:
def read_large_file(file_path):
with open(file_path, 'r') as file:
for line in file:
yield line.strip()
# Process a large file line by line without loading it all into memory
for line in read_large_file('large_log_file.txt'):
if 'ERROR' in line:
print(f"Found error: {line}")
Data processing pipeline:
def get_data():
# Simulating data source
for i in range(100):
yield i
def process_data(data_source):
for item in data_source:
# Process each item
yield item * 2
def filter_data(data_source):
for item in data_source:
if item % 3 == 0: # Only keep multiples of 3
yield item
# Create a data processing pipeline
pipeline = filter_data(process_data(get_data()))
# Process the first 10 results
for _ in range(10):
print(next(pipeline), end=" ") # 0 6 12 18 24 30 36 42 48 54
Infinite sequence:
def infinite_sequence():
num = 0
while True:
yield num
num += 1
# Take just what we need from an infinite sequence
counter = infinite_sequence()
for _ in range(5):
print(next(counter), end=" ") # 0 1 2 3 4
6. Generator Methods and Advanced Features (15 minutes)
Explain additional generator features:
Sending values to generators:
def echo_generator():
while True:
received = yield
print(f"Received: {received}")
echo = echo_generator()
next(echo) # Prime the generator
echo.send("Hello") # Received: Hello
echo.send(42) # Received: 42
Returning values from generators:
def gen_with_return():
yield 1
yield 2
yield 3
return "Done!"
g = gen_with_return()
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
try:
print(next(g))
except StopIteration as e:
print(f"Generator returned: {e.value}") # Generator returned: Done!
Generator methods:
def generator_demo():
value = yield "Start"
print(f"Received: {value}")
yield "Middle"
yield "End"
# Create and start the generator
gen = generator_demo()
result = next(gen)
print(f"First yield: {result}") # First yield: Start
# Send a value
result = gen.send("Hello")
print(f"After send: {result}") # After send: Middle
# Close the generator
gen.close()
try:
next(gen)
except StopIteration:
print("Generator is closed")
7. Guided Practice (20 minutes)
Have students work through these exercises:
-
Create a generator for powers of 2:
def powers_of_two(max_exponent): exponent = 0 while exponent <= max_exponent: yield 2 ** exponent exponent += 1 # Test the generator for power in powers_of_two(10): print(power, end=" ") # 1 2 4 8 16 32 64 128 256 512 1024 -
Implement a generator that yields dates for each day in a given month:
import datetime def days_in_month(year, month): """Generate all days in the specified month and year.""" # Create a date for the first day of the month current_date = datetime.date(year, month, 1) # Keep yielding dates until we move to the next month while current_date.month == month: yield current_date current_date += datetime.timedelta(days=1) # Print all days in July 2023 for day in days_in_month(2023, 7): print(day.strftime("%Y-%m-%d"), end=", ")
8. Problem-Solving Activities (15 minutes)
Present students with more challenging problems:
-
Implement a generator that yields prime numbers:
def primes(): """Generator that yields an infinite sequence of prime numbers.""" # Start with 2, the first prime yield 2 # Check odd numbers starting from 3 n = 3 while True: if all(n % i != 0 for i in range(2, int(n**0.5) + 1)): yield n n += 2 # Get the first 10 prime numbers prime_gen = primes() for _ in range(10): print(next(prime_gen), end=" ") # 2 3 5 7 11 13 17 19 23 29 -
Create a generator for a "running average":
def running_average(): """Yields the running average of values sent to the generator.""" total = 0 count = 0 average = 0 while True: # Receive a new value from the generator's caller value = yield average # Update running average total += value count += 1 average = total / count # Test the running average generator avg_gen = running_average() next(avg_gen) # Prime the generator print(avg_gen.send(10)) # 10.0 print(avg_gen.send(20)) # 15.0 print(avg_gen.send(30)) # 20.0 print(avg_gen.send(40)) # 25.0
9. Review and Discussion (10 minutes)
- Review the key concepts covered
- Ask students to explain in their own words:
- What are generators and how do they differ from regular functions?
- When would you use a generator instead of a list?
- What is lazy evaluation and why is it useful?
Common Challenges and Solutions
- Conceptual understanding: Use visual diagrams to show how generators pause and resume.
- StopIteration exceptions: Explain that these are normal and are handled automatically in
forloops. - Single-use iterators: Remind students that generators can only be iterated through once.
- Memory tracking: Show real memory usage stats to drive home the efficiency point.
Extension Activities
For students who finish early:
- Challenge them to implement a generator-based version of a CSV parser
- Have them create a data-streaming simulation using generators
- Ask them to optimize an existing function using generators
Assessment
Look for these indicators of understanding:
- Students can create generator functions using
yield - They can explain the benefits of generators over lists
- They correctly use generator expressions in appropriate situations
- They understand how generators maintain state between calls