Generators
A generator function automatically implements the iterator protocol using yield instead of return, without any of the boilerplate seen in Iterators.
Basic Generator Function
def count_up(start, end):
current = start
while current <= end:
yield current # pauses here, remembers state, resumes on next call
current += 1
for n in count_up(1, 5):
print(n) # 1 2 3 4 5Calling a generator function does not run its body immediately, it returns a generator OBJECT.
gen = count_up(1, 3)
gen # <generator object count_up at 0x...>
next(gen) # 1, execution runs until the first yield
next(gen) # 2
next(gen) # 3
next(gen) # StopIterationHow yield Pauses and Resumes
Each call to next() resumes execution right after the last yield, running until the next yield or the function ends.
def demo():
print("start")
yield 1
print("middle")
yield 2
print("end")
g = demo()
next(g) # prints 'start', returns 1
next(g) # prints 'middle', returns 2
next(g) # prints 'end', then raises StopIterationWhy Use Generators: Lazy Evaluation and Memory Efficiency
def all_numbers(): # generates values forever, no memory blowup
n = 0
while True:
yield n
n += 1
def first_n(iterable, n):
it = iter(iterable)
return [next(it) for _ in range(n)]
first_n(all_numbers(), 5) # [0, 1, 2, 3, 4], only 5 values ever computed# Reading a huge file line by line without loading it all into memory
def read_large_file(path):
with open(path) as f:
for line in f:
yield line.strip()
for line in read_large_file("huge_log.txt"):
process(line) # each line generated on demand, file never fully loadedGenerators vs Lists
Use a generator when you will consume values once, in order, and do not need random access,
len(), or to re-iterate. Use a list when you need to index into it, check its length, iterate multiple times, or pass it to something that requires a concrete sequence.
Generator Expressions (Recap)
squares_gen = (x**2 for x in range(10)) # see [[Comprehensions]]
sum(x**2 for x in range(1000)) # memory efficient sum, no intermediate listyield from: Delegating to a Sub-Generator
def inner():
yield 1
yield 2
def outer():
yield "start"
yield from inner() # delegates, yielding 1 then 2 as if outer yielded them directly
yield "end"
list(outer()) # ['start', 1, 2, 'end']def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item) # recursive generator delegation
else:
yield item
list(flatten([1, [2, 3, [4, 5]], 6])) # [1, 2, 3, 4, 5, 6]Sending Values Into a Generator
yield can also be an expression that receives a value via .send(), enabling two-way communication (advanced, rarely needed in everyday code).
def echo():
while True:
received = yield
print(f"Got: {received}")
gen = echo()
next(gen) # prime the generator, advances to the first yield
gen.send("hi") # prints 'Got: hi'
gen.send("bye") # prints 'Got: bye'Generators and Exceptions
def gen():
try:
yield 1
yield 2
finally:
print("Cleanup ran")
g = gen()
next(g)
g.close() # prints 'Cleanup ran', stops the generator earlyInfinite Generators with itertools
import itertools
counter = itertools.count(start=1, step=2) # 1, 3, 5, 7, ... forever
cycler = itertools.cycle(["A", "B", "C"]) # A, B, C, A, B, C, ... foreverSee Itertools-and-Functools for the full toolkit built around this pattern.