Python Generators and Iterators: The Power of Lazy Evaluation
Learn about iterators and generators in Python and how they can help optimize memory usage in your programs.

Python Generators and Iterators: The Power of Lazy Evaluation
Python's iterators and generators are powerful features that enable lazy evaluation, allowing you to work with large datasets efficiently without loading everything into memory at once.
Understanding Iterators
In Python, an iterator is an object that represents a stream of data, allowing you to traverse through all the elements.
Iterator Protocol
The iterator protocol consists of two methods:
__iter__()
: Returns the iterator object itself__next__()
: Returns the next value from the iterator, raisingStopIteration
when there are no more items
PYTHON1# Creating an iterator from a list 2my_list = [1, 2, 3, 4, 5] 3my_iterator = iter(my_list) 4 5# Iterating through the iterator 6print(next(my_iterator)) # Output: 1 7print(next(my_iterator)) # Output: 2 8 9# Using a for loop (which automatically calls next()) 10for item in iter(my_list): 11 print(item)
Creating Custom Iterators
You can create your own iterators by implementing the iterator protocol:
PYTHON1class Countdown: 2 def __init__(self, start): 3 self.start = start 4 5 def __iter__(self): 6 return self 7 8 def __next__(self): 9 if self.start <= 0: 10 raise StopIteration 11 self.start -= 1 12 return self.start + 1 13 14# Using our custom iterator 15for number in Countdown(5): 16 print(number) # Outputs: 5, 4, 3, 2, 1
Understanding Generators
Generators are a simple way to create iterators using functions and the yield
statement. They automatically implement the iterator protocol for you.
Generator Functions
A generator function is defined like a normal function but uses yield
instead of return
to provide a value:
PYTHON1def countdown(start): 2 while start > 0: 3 yield start 4 start -= 1 5 6# Using the generator 7for number in countdown(5): 8 print(number) # Outputs: 5, 4, 3, 2, 1
When a generator function is called, it returns a generator object without executing the function body. The function only executes when next()
is called on the generator object.
Generator Expressions
Generator expressions are similar to list comprehensions but create generators instead of lists:
PYTHON1# List comprehension (creates the entire list in memory) 2squares_list = [x**2 for x in range(1000000)] 3 4# Generator expression (creates values on-the-fly) 5squares_gen = (x**2 for x in range(1000000)) 6 7# The generator expression is more memory-efficient 8print(next(squares_gen)) # Output: 0 9print(next(squares_gen)) # Output: 1
Benefits of Lazy Evaluation
Memory Efficiency
Generators and iterators produce values on-demand, which means they don't need to store all values in memory at once:
PYTHON1# This would consume a lot of memory for large numbers 2def get_all_numbers(n): 3 result = [] 4 for i in range(n): 5 result.append(i) 6 return result 7 8# This is memory-efficient regardless of how large n is 9def get_numbers_generator(n): 10 for i in range(n): 11 yield i
Working with Infinite Sequences
Generators can represent infinite sequences, which would be impossible with regular lists:
PYTHON1def fibonacci(): 2 a, b = 0, 1 3 while True: 4 yield a 5 a, b = b, a + b 6 7# Get the first 10 Fibonacci numbers 8fib_gen = fibonacci() 9for _ in range(10): 10 print(next(fib_gen))
Advanced Generator Features
Sending Values to Generators
Generators can receive values using the send()
method:
PYTHON1def echo(): 2 value = yield 3 while True: 4 value = yield f"Got: {value}" 5 6gen = echo() 7next(gen) # Prime the generator 8print(gen.send("Hello")) # Output: Got: Hello 9print(gen.send(42)) # Output: Got: 42
Generator Delegation with yield from
The yield from
statement allows you to delegate part of a generator's operations to another generator:
PYTHON1def subgenerator(): 2 yield 1 3 yield 2 4 yield 3 5 6def main_generator(): 7 yield "Start" 8 yield from subgenerator() # Delegate to subgenerator 9 yield "End" 10 11for item in main_generator(): 12 print(item) # Outputs: Start, 1, 2, 3, End
Closing Generators
Generators can be closed using the close()
method, which raises a GeneratorExit
exception inside the generator:
PYTHON1def closeable_generator(): 2 try: 3 yield 1 4 yield 2 5 yield 3 6 except GeneratorExit: 7 print("Generator closed!") 8 9gen = closeable_generator() 10print(next(gen)) # Output: 1 11gen.close() # Output: Generator closed!
Practical Examples
Reading Large Files
Generators are perfect for processing large files line by line without loading the entire file into memory:
PYTHON1def read_large_file(file_path): 2 with open(file_path, 'r') as file: 3 for line in file: 4 yield line.strip() 5 6# Process a large log file efficiently 7for line in read_large_file('huge_log.txt'): 8 if 'ERROR' in line: 9 print(f"Found error: {line}")
Data Pipeline Processing
Generators can be used to create data processing pipelines:
PYTHON1def read_data(file_path): 2 with open(file_path, 'r') as file: 3 for line in file: 4 yield line.strip() 5 6def parse_data(lines): 7 for line in lines: 8 yield line.split(',') 9 10def filter_data(rows, keyword): 11 for row in rows: 12 if keyword in row[0]: 13 yield row 14 15# Create a processing pipeline 16lines = read_data('data.csv') 17parsed_data = parse_data(lines) 18filtered_data = filter_data(parsed_data, 'important') 19 20# Process the data 21for item in filtered_data: 22 print(item)
Best Practices
- Use generators for large datasets: When dealing with large amounts of data, prefer generators over lists
- Chain generators together: Create pipelines of generators for complex data processing
- Consider memory usage: Be aware that generators save memory by not storing all values at once
- Be careful with side effects: Remember that generator functions execute incrementally
Conclusion
Iterators and generators are powerful Python features that enable efficient processing of data sequences. By using lazy evaluation, they allow you to work with large datasets without excessive memory usage and create elegant solutions for data processing problems.
Mastering these concepts will help you write more efficient and elegant Python code, especially when dealing with large datasets or complex data processing pipelines.