Generators and Iterators
Generators and iterators let Python give you values one at a time (this is called lazy evaluation) instead of building the entire collection in memory all at once.
This becomes really important when:
- The data is too large (like files, streams, or APIs)
- The values never end, or you don’t know how many there will be
- You only need some of the results, not all of them
Iterables vs. Iterators
Section titled “Iterables vs. Iterators”Iterable
Section titled “Iterable”An iterable is simply anything you can loop over.
Examples:
- list
- tuple
- string
- set
- dictionary
- file
nums = [1, 2, 3]for n in nums: print(n)Iterator
Section titled “Iterator”An iterator is an object that:
- Remembers where it currently is
- Gives you the next value whenever you ask for it
Iterator Protocol (The Rules an Iterator Follows)
Section titled “Iterator Protocol (The Rules an Iterator Follows)”An object counts as an iterator if it has these two things:
__iter__()-> gives back the iterator object itself__next__()-> gives back the next value- It raises
StopIterationonce there’s nothing left
Doing Iteration by Hand
Section titled “Doing Iteration by Hand”nums = [10, 20, 30]
it = iter(nums)
print(next(it)) # 10print(next(it)) # 20print(next(it)) # 30# next(it) -> StopIterationMaking Your Own Iterator
Section titled “Making Your Own Iterator”class CountDown: def __init__(self, start): self.current = start
def __iter__(self): return self
def __next__(self): if self.current <= 0: raise StopIteration value = self.current self.current -= 1 return value
for num in CountDown(3): print(num)Generators
Section titled “Generators”Generators are a much easier way to build an iterator.
Instead of writing a whole class, you just use the yield keyword.
The Main Idea
Section titled “The Main Idea”return-> ends the function completelyyield-> pauses the function for a moment and remembers exactly where it left off
Example of a Generator Function
Section titled “Example of a Generator Function”def generate_numbers(n): i = 1 while i <= n: yield i i += 1
gen = generate_numbers(3)
for num in gen: print(num)Execution Flow
Section titled “Execution Flow”Things to Remember
Section titled “Things to Remember”- The function does NOT start running the moment you call it
- It only runs when you call
next()on it - It remembers its state (where it stopped) between each call
Generators vs. Lists
Section titled “Generators vs. Lists”# List (memory heavy)nums = [x*x for x in range(1000000)]
# Generator (memory efficient)nums = (x*x for x in range(1000000))A generator:
- Only works out a value when it’s actually needed
- Uses very little memory, no matter how big the sequence is
Generator Expressions
Section titled “Generator Expressions”This is a short and quick way to write a generator, similar to how list comprehensions work.
gen = (x * 2 for x in range(5))
for val in gen: print(val)Infinite Generators
Section titled “Infinite Generators”Generators can also create sequences that never end.
Example: A Counter That Never Stops
Section titled “Example: A Counter That Never Stops”def infinite_counter(): num = 1 while True: yield num num += 1
counter = infinite_counter()
for _ in range(5): print(next(counter))Output
Section titled “Output”12345Picture It Like This
Section titled “Picture It Like This”Sending Values Into a Generator
Section titled “Sending Values Into a Generator”You can also send input back into a generator using send().
Example
Section titled “Example”def accumulator(): total = 0 while True: value = yield total total += value
acc = accumulator()
print(next(acc)) # start generatorprint(acc.send(10)) # 10print(acc.send(5)) # 15Important Rule to Remember
Section titled “Important Rule to Remember”Closing a Generator
Section titled “Closing a Generator”When you call close() on a generator:
- Python raises a
GeneratorExiterror inside the generator - This gives you a chance to clean things up properly
Why Do We Need to Close Generators?
Section titled “Why Do We Need to Close Generators?”Example
Section titled “Example”def resource_manager(): print("Resource opened") count = 0
try: while True: yield count count += 1 except GeneratorExit: print(f"Cleaning up... used {count} times")
gen = resource_manager()
print(next(gen))print(next(gen))
gen.close()Output
Section titled “Output”Resource opened01Cleaning up... used 2 timesAn Interactive Generator Example
Section titled “An Interactive Generator Example”def order_processor(): total_orders = 0 print("System Ready")
try: order = yield
while True: print(f"Processing: {order}") total_orders += 1 order = yield
except GeneratorExit: print(f"System Shutdown. Total orders: {total_orders}")
processor = order_processor()
next(processor) # start
processor.send("Pizza")processor.send("Burger")
processor.close()yield from Delegation
Section titled “yield from Delegation”This is used to make working with nested generators much simpler.
Without Using yield from
Section titled “Without Using yield from”def chain(): for x in [1, 2]: yield x for y in [3, 4]: yield yWith yield from
Section titled “With yield from”def chain(): yield from [1, 2] yield from [3, 4]