Generators
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.
Click Run to execute your code
- 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!
Click Run to execute your code
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!
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.
Click Run to execute your code
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
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
yieldinstead ofreturn, 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!
Enjoying these tutorials?