FunctionsChapter 57 of 114
Generators
Produce values one at a time instead of building a whole list.
yield makes a generator
A function with yield in it does not run when you call it. It hands back a generator, and the body runs a piece at a time as values are asked for:
def counter():
print("starting")
yield 1
print("between")
yield 2
print("finishing")
gen = counter()
print(type(gen).__name__)
print(next(gen))
print(next(gen))Output
generator starting 1 between 2
Note "starting" did not print until the first next(). The function is paused between yields, keeping its variables exactly where they were.
Usually you just loop
def countdown(n):
while n > 0:
yield n
n -= 1
for value in countdown(3):
print(value)
print(list(countdown(3)))
print(sum(countdown(4)))Output
3 2 1 [3, 2, 1] 10
Why bother: memory
A list holds every value at once. A generator holds one. For a large input that is the difference between working and not:
def squares_list(n):
return [i * i for i in range(n)]
def squares_gen(n):
for i in range(n):
yield i * i
print(sum(squares_list(1000)))
print(sum(squares_gen(1000)))
print(sum(i * i for i in range(1000)))Output
332833500 332833500 332833500
Same answer three ways. The last two never build the list.
Why bother: it can be endless
def naturals():
n = 1
while True:
yield n
n += 1
def take(source, count):
for index, value in enumerate(source):
if index >= count:
return
yield value
print(list(take(naturals(), 5)))Output
[1, 2, 3, 4, 5]
naturals() never ends, and asking for five costs five. A list version would never return.
Generator expressions
The comprehension form, with round brackets:
squares = (n * n for n in range(5))
print(type(squares).__name__)
print(list(squares))Output
generator [0, 1, 4, 9, 16]
Inside a call the extra brackets are unnecessary:
print(sum(n * n for n in range(5)))
print(max(len(w) for w in ["a", "abc", "ab"]))
print(any(n > 3 for n in [1, 2, 5]))Output
30 3 True
any and all stop at the first decisive value, so pairing them with a generator means the rest is never computed.
They are used up
This is the one thing that catches everyone:
squares = (n * n for n in range(4))
print(sum(squares))
print(sum(squares))
print(list(squares))Output
14 0 []
The first sum consumed it. zip, map, filter and enumerate behave the same way. Wrap in list() if you need it twice.
yield from
Delegates to another generator, which keeps nested iteration flat:
def letters():
yield "a"
yield "b"
def both():
yield from letters()
yield from [1, 2]
print(list(both()))Output
['a', 'b', 1, 2]
Reading a large file
The pattern generators exist for. Nothing here holds more than one line:
with open("log.txt", "w", encoding="utf-8") as f:
f.write("ok 1\nerror 2\nok 3\nerror 4\n")
def lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.strip()
def only(source, word):
for line in source:
if line.startswith(word):
yield line
print(list(only(lines("log.txt"), "error")))Output
['error 2', 'error 4']
Each stage takes a generator and returns one. The file is read once, lazily, however large it is.
Test yourself
2 questionsWhen does the body of a generator function first run?
Show the answer
On the first next(), not when you call it — Calling it builds a generator and runs nothing. That laziness is the whole point.
Why does the second sum() over the same generator give 0?
Show the answer
The first one used it up — zip, map, filter and enumerate all behave this way. Wrap in list() if you need it twice.
Closures
A function that remembers the variables it was built with.