Iterators and generators
Part of the free LogicWiz Python course, Module 12: Iteration and the standard toolkit. The examples and practice run in the app.
The canteen's sales log has grown to a crore lines. Loading it all into a list would fill Aarav's phone memory. A generator reads one line, hands it over, and waits.
You'll learn to
- Explain how a `for` loop really works with `iter()`, `next()` and `StopIteration`
- Write an iterator class with `__iter__` and `__next__`
- Write generators with `yield` and generator expressions
- Use laziness to save memory, and remember a generator works only once
Learn
Iterable vs iterator
An iterable is anything you can loop over: a list, string, dict, file, range.
An iterator is the object that actually walks through it, one item at a time, remembering where it is.
iter(thing)gives you an iteratornext(it)gives the next item- when nothing is left,
nextraisesStopIteration
A for loop does exactly this for you, and quietly stops at StopIteration.
A for loop, by hand
tokens = ["A1", "A2"]
it = iter(tokens)
print(next(it))
print(next(it))
try:
next(it)
except StopIteration:
print("no more tokens")
The iterator protocol: __iter__ and __next__
class Countdown:
def __init__(self, start):
self.n = start
def __iter__(self):
return self # the object is its own iterator
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n + 1
for x in Countdown(3):
print(x)
Generators: yield does the work
Writing a class is a lot of code. A generator function is a normal def that uses yield instead of return.
- Calling it does not run the body. It returns a generator object.
- Each
next()runs the body until the nextyield, hands that value out, and pauses there, keeping all its local variables. - When the body ends, the generator raises
StopIterationfor you.
Watch the pause
def countdown(n):
print("starting")
while n > 0:
yield n
n -= 1
print("done")
g = countdown(3)
print("made the generator")
print(next(g))
print(next(g))
print(list(g))
"starting" appears only at the first next(). Nothing runs until someone asks for a value.
Generator expressions save memory
import sys
as_list = [n * n for n in range(100000)]
as_gen = (n * n for n in range(100000))
print(sys.getsizeof(as_list), "bytes for the list")
print(sys.getsizeof(as_gen), "bytes for the generator")
print(sum(as_gen))
print(sum(x for x in range(1, 11)))
Round brackets make a generator. Inside a call like sum(...), you can drop the extra brackets.
An endless token machine
def token_numbers(start):
n = start
while True: # never ends, and that's fine
yield n
n += 1
tokens = token_numbers(101)
print(next(tokens), next(tokens), next(tokens))
A list can never be infinite. A generator can, because it only makes the next value when asked.
A generator can be used only once. After you loop through it (or call sum, list, max on it), it is empty. A second loop gives nothing, with no error. If you need the values twice, make a list: data = list(gen), or call the generator function again.
In one line
for uses iter() and next() until StopIteration; a generator (yield or (x for x in …)) makes values lazily, saves memory and can be used only once.
Interview corner
What is the difference between an iterable and an iterator?
An iterable has __iter__ and can produce an iterator (lists, strings, dicts). An iterator has __next__ (and __iter__ returning itself) and remembers its position; next() gives the next item and raises StopIteration at the end. A list is iterable but not an iterator: next([1, 2]) is a TypeError.
What does yield do? How is a generator different from a function that returns a list?
yield makes the function a generator: calling it returns a generator object without running the body. Each next() runs to the next yield, hands out the value and pauses with its state saved. Compared with returning a list, a generator is lazy, uses constant memory, can be infinite, and can be consumed only once.
When would you choose a generator over a list?
When the data is big or endless (log files, streams), when you only need one pass (feeding sum, max, any, or another loop), or when you may stop early. Use a list when you need indexing, len(), or several passes over the same data.
Why does looping over the same generator a second time give nothing?
A generator is an iterator: it moves forward only and is exhausted after the first full pass. Later loops get StopIteration immediately, so they run zero times silently. Store the values with list(gen) or call the generator function again for a fresh one.