Iteration: iter, next and lazy generators
What a Python for loop really does: iter() and next() until StopIteration. Write an iterator, then a generator with yield, and watch it do its work lazily.
A for loop works on lists, strings, dicts, files and much more, and it does it with just two built-in functions. This loop:
for colour in ["red", "amber", "green"]:
print(colour)does the same as this:
lights = iter(["red", "amber", "green"])
while True:
try:
colour = next(lights)
except StopIteration:
break
print(colour)iter(x)asksxfor an iterator, an object that hands out items one at a time;next(it)asks the iterator for the next item;- when there are none left,
nextraisesStopIteration, and the loop ends quietly.
An iterable is anything iter() works on: a list is iterable, but it isn't an iterator itself. An iterator remembers where it is, and it gets used up:
letters = iter("abc")
print(next(letters)) # a
print(list(letters)) # ['b', 'c']: the rest
print(list(letters)) # []: nothing leftWriting an iterator
A class becomes an iterator with two special methods: __next__ gives the next item (or raises StopIteration), and __iter__ returns the iterator itself, so it works in a for loop too:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
print(list(Countdown(3))) # [3, 2, 1]Generators: the short way
A function with yield in it is a generator function. Calling it doesn't run its body: it returns a generator, an iterator that runs the body a step at a time. Each next() runs until the next yield, hands that value over and pauses right there, with its local variables kept. The prints show you exactly when each part runs:
def slow_squares(n):
print(" (starting)")
for i in range(n):
print(f" (working out {i} squared)")
yield i * i
print(" (finished)")
gen = slow_squares(3)
print("Made the generator. Nothing has run yet.")
print("first:", next(gen))
for value in gen:
print("loop got", value)Made the generator. Nothing has run yet. (starting) (working out 0 squared) first: 0 (working out 1 squared) loop got 1 (working out 2 squared) loop got 4 (finished)
That's what lazy means: no value is worked out before it's asked for. A generator can describe a million items, or an endless stream, while holding only one at a time. A generator expression is the one-line form, written like a list comprehension with round brackets:
total = sum(n * n for n in range(1_000_000)) # no million-item list is built print(total)
More in the Python docs: iterator types, generators and the yield expression.
Your turn
Write a generator function odd_squares(limit) that yields the square of each odd number below limit, one at a time (so odd_squares(6) gives 1, 9, 25). Make gen = odd_squares(10), take its first value with next() and print it, then print list(gen) to see what’s left.
Your task
Write a generator function odd_squares(limit) that yields the square of each odd number below limit, one at a time (so odd_squares(6) gives 1, 9, 25). Make gen = odd_squares(10), take its first value with next() and print it, then print list(gen) to see what’s left.
- odd_squares() returns a generator (not done yet)
- It yields 1, 9, 25, 49 and 81 for limit 10 (not done yet)
- Take the first value with next() and print it (not done yet)
- Print what’s left: [9, 25, 49, 81] (not done yet)
The checklist ticks itself off as you work in the editor: any way that gets the result counts.
Hints come one at a time, then one way to do it. Try each before the next.
What this lesson uses, in one place.
iter()- Gets an iterator from an iterable (a list, string, dict…) Python docs →
next()- The next item from an iterator; raises StopIteration when there are none left Python docs →
StopIteration- Raised by next() when an iterator is used up; for loops catch it and stop Python docs →
__iter__ / __next__- The iterator protocol: __iter__ returns an iterator, __next__ gives each item Python docs →
yield- Makes a function a generator: hands over a value and pauses until the next one is asked for Python docs →
(x for x in xs)- A generator expression: lazy, one item at a time, with no list built Python docs →