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 yield statement instead of return
  • 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 yield statement is what makes a function a generator
  • Each call to next() executes the function until it reaches a yield statement
  • The function "pauses" at the yield and 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:

  1. 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
    
  2. 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:

  1. 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
    
  2. 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 for loops.
  • 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

Resources

Chapter 1: Generators | Teacher's Guide