Web Analytics

Generators

Advanced ~25 min read

Generators provide an elegant way to create iterators without the boilerplate of __iter__ and __next__ methods. Using the yield keyword, generator functions automatically implement the iterator protocol. They're perfect for memory-efficient iteration, infinite sequences, and lazy evaluation - generating values only when needed!

Generator Functions and yield

A generator function looks like a regular function, but uses yield instead of return. When called, it returns a generator object (an iterator) without executing the function body immediately. Each call to next() resumes execution until the next yield, which produces a value and pauses the function.

Output
Click Run to execute your code
How yield Works:
- When a generator function is called, it returns a generator object without executing
- Calling next() executes until the first yield
- The function pauses at yield and returns the yielded value
- Next call to next() resumes after yield and continues until the next yield or function ends
- When the function ends or returns, StopIteration is raised automatically

Memory Efficiency: Generators vs Lists

Generators generate values on-demand, using minimal memory. Compare this to lists that must store all values in memory at once. For large datasets, generators are essential!

Output
Click Run to execute your code
Pro Tip: Use generators when you don't need all values at once or when dealing with large datasets. You can convert a generator to a list with list(generator) if you need random access, but this defeats the memory advantage!

Generator Expressions

Generator expressions are like list comprehensions but create generators instead of lists. They use parentheses () instead of square brackets []. Perfect for simple generator creation!

Output
Click Run to execute your code

Infinite Sequences

Since generators produce values lazily, they can represent infinite sequences that would be impossible to store in memory. This is perfect for mathematical sequences or continuous data streams.

Output
Click Run to execute your code
Caution: Infinite generators can loop forever if not handled carefully. Always use break conditions or limit the number of values consumed. Never try to convert an infinite generator to a list - it will consume all memory!

Common Mistakes

1. Trying to reuse a consumed generator

# Wrong - generator is exhausted after first iteration
def numbers():
    yield 1
    yield 2
    yield 3

gen = numbers()
print(list(gen))  # [1, 2, 3]
print(list(gen))  # [] - empty!

# Correct - create a new generator each time
def numbers():
    yield 1
    yield 2
    yield 3

print(list(numbers()))  # [1, 2, 3]
print(list(numbers()))  # [1, 2, 3] - new generator

2. Converting infinite generators to lists

# Wrong - will consume all memory and crash!
def infinite_numbers():
    i = 0
    while True:
        yield i
        i += 1

all_numbers = list(infinite_numbers())  # Never completes!

# Correct - use generator directly with limits
def infinite_numbers():
    i = 0
    while True:
        yield i
        i += 1

# Use with itertools.islice or for loop with break
for num in infinite_numbers():
    if num > 10:
        break
    print(num)

3. Confusing return and yield

# Wrong - return ends the generator immediately
def bad_generator():
    yield 1
    return 2  # Generator stops here, never yields 3
    yield 3

# Correct - use yield for all values, return is optional for cleanup
def good_generator():
    yield 1
    yield 2
    yield 3
    # Optional: return value (accessed via StopIteration.value)

Exercise: Fibonacci Generator

Task: Create a generator function that produces Fibonacci numbers. The Fibonacci sequence starts with 0, 1, and each subsequent number is the sum of the previous two.

Requirements:

  • Create a fibonacci() generator function
  • Yield numbers starting from 0, then 1, then continue the sequence
  • The function should be able to generate numbers indefinitely
  • Test it by printing the first 10 Fibonacci numbers
Output
Click Run to execute your code
Show Solution
def fibonacci():
    """Generator that yields Fibonacci numbers indefinitely."""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b


# Print first 10 Fibonacci numbers
print("First 10 Fibonacci numbers:")
fib = fibonacci()
for i in range(10):
    print(next(fib))

# Or using a for loop with enumerate
print("\nUsing for loop with break:")
for i, num in enumerate(fibonacci()):
    if i >= 10:
        break
    print(f"F({i}) = {num}")

Summary

  • Generator Functions: Functions that use yield instead of return, automatically creating iterators
  • yield: Pauses function execution, returns a value, and resumes on next call
  • Memory Efficient: Generators produce values on-demand, perfect for large datasets
  • Generator Expressions: Syntax (expr for item in iterable) creates generators like list comprehensions create lists
  • Lazy Evaluation: Values are generated only when needed, not all at once
  • Infinite Sequences: Generators can represent infinite sequences impossible to store in memory
  • One-time Use: Generators are consumed after iteration; create new ones to iterate again
  • Under the Hood: Generators automatically implement __iter__ and __next__ methods

What's Next?

Generators are powerful tools for iteration! Next, we'll explore decorators, which allow you to modify or extend function behavior without permanently changing the function itself. Decorators are commonly used with generators for timing, logging, and caching!