Programming and IT
Python: generators
A generator hands out values one at a time, and only when someone asks for them. It does not keep the whole sequence in memory, so it suits large files and endless streams. The price is that a generator is single-use: you cannot loop over it twice.
A function that pauses itself#
An ordinary function runs until return and forgets its state. A function with
yield behaves differently: it hands out a value and freezes, keeping
everything — where it stopped and its local variables — until the next request.
yield
-= 1
=
# <generator object countdown at 0x...>
# 3
# 2
# 1
# prints "done", then StopIteration
Calling countdown(3) does not run a single line of the body — it only creates a
generator object. The first line runs on the first next. When the body reaches
its end, the generator raises StopIteration; a for loop catches that exception
itself, so you do not see it in ordinary code:
# 3 2 1 done
Generator expressions#
If the logic fits in one expression, you do not need a separate function. The syntax is the same as a list comprehension, but in parentheses:
= # a list, computed in full
= # a generator, nothing computed yet
# [0, 1, 4, 9, 16]
# <generator object ...>
# 30
When a generator expression is the only argument of a function, you can drop the
extra parentheses: sum(x * x for x in range(5)). This is the most common use:
computing a sum, a maximum or any/all without building a list.
=
# True
# 1
Why you need them#
Memory. A list comprehension over a million items creates a million objects at
once; a generator holds one. You can see the difference with sys.getsizeof: a
list's size grows with its length, a generator's stays constant.
Early exit. If the answer is found at the third item, the rest are never computed:
yield * 2
break
# computing 1 / computing 2 / stopped
Infinity. A generator may never end — that is fine as long as something else limits it:
= 1
yield
+= 1
# [1, 2, 3, 4, 5]
yield from passes along every value of another generator or sequence without
an explicit loop: yield from range(3) yields 0, 1, 2.
Pitfalls#
A generator is single-use. Once you have gone through it, it is empty:
=
# [1, 2, 3]
# []
This bites in code that first prints the result and then tries to count it. If you need to process the data again, save it to a list or create the generator anew.
len does not work. A generator has no length: len(gen) raises
TypeError: object of type 'generator' has no len(). You can count items only by
going through them: sum(1 for _ in gen) — and after that the generator is empty.
No indexing either. gen[0] is impossible; use next(gen) or
itertools.islice.
Late binding. A generator expression reads outer variables at the moment it is iterated, not when it is created. If a variable changes in between, the result changes too.
A short exercise: write a generator read_numbers(path) that yields one number at
a time from a file, and add them up with sum(...) without building a list.
Reading line by line is covered in the article on
reading files in Python, and how functions with
parameters work is in the article on Python functions.
Step-by-step plan
- Your first yieldWrite a generator of three values and call next four times until you get StopIteration.
- Compare with a listBuild the same data with a list comprehension and a generator expression and print both objects.
- Count without a listFind the sum of squares of the first thousand numbers using sum with a generator expression.
- See the lazinessPut a print inside the generator and break out of the loop early — check that nothing extra was computed.
- Hit the single-use trapLoop over a generator twice in a row and explain why the second pass is empty.
Start learning this in your own space
The plan goes into your repository: tick off stages, keep notes — the change history shows how far you have come.
Check yourself
1.What does this print: g = (x for x in [1, 2, 3]); list(g); print(len(list(g)))?
2.What does sum(x * x for x in range(5)) return?
3.What does print(type((x for x in range(3))).__name__) print?
4.How many values does this generator yield before StopIteration: def g(): yield from range(4)?
Sources
-
Generators in the Python tutorialThe Iterators, Generators and Generator Expressions sectionsfree
-
The itertools moduleislice, count, chain and other ready-made generatorsfree
-
Python glossaryExact definitions of iterator, generator and iterablefree
Was this helpful?