Producing Values One at a Time
A list built to hold a million numbers holds all million in memory at once, whether you need them yet or not. Write a generator instead, pull one value at a time, and watch it produce the next number only when asked.
Worth reading first: Naming a Piece of Work
A list built to hold a million numbers holds all million in memory at once, whether you need them yet or not. Write a generator instead, pull one value at a time, and watch it produce the next number only when asked.
A list you already have, versus a value you have not made yet
[n for n in range(1_000_000)] computes all one million values immediately and holds every one in memory before the line even finishes. If the program only ever needed the first three, the other 999,997 were wasted work.
$ >>> def fibonacci_below(limit):$ ... a, b = 1, 1$ ... while a < limit:$ ... yield a$ ... a, b = b, a + b$ ...$ >>> gen = fibonacci_below(20)$ >>> gen$ <generator object fibonacci_below at 0x1046a3...>Calling fibonacci_below(20)runs none of the function's body. It returns a generator — an object that remembers where to resume, but has not computed a single value yet.
yield pauses a function instead of ending it
$ >>> next(gen)$ 1$ >>> next(gen)$ 1$ >>> next(gen)$ 2Every next(gen) call resumes the function exactly where yield last paused it, runs until the next yield, and hands back that one value. return would end the function for good — yield only pauses it.
def fibonacci_below(limit):
a, b = 1, 1
while a < limit:
yield a
a, b = b, a + bNothing has run yet. Calling fibonacci_below(20) doesn't compute anything — it just creates a paused generator.
Why laziness is the entire point
A generator that never finishes, like one counting upward forever, would be impossible as a list — there would be no last value to stop at. As a generator it works fine, because nothing is computed until something actually asks next() for it.
Key takeaways
- A generator function contains yield and returns a paused generator object the moment it's called — none of its body has run yet.
- next(generator) resumes the function until the next yield, then pauses again and hands back that one value.
- Unlike return, yield doesn't end the function — the next next() call picks up right where it left off.
- A generator computes values one at a time on demand, which is the only way to represent a sequence that's huge or never-ending.