Lesson 3 of 10
You've used for since Module 2 without knowing how it works. The answer is the iterator
protocol — two methods that any object can implement to become loopable.
Iterable versus iterator
- An iterable can produce an iterator. It has
__iter__(). Lists, strings, dicts, files. - An iterator produces values one at a time. It has
__next__()(and an__iter__()that returns itself).
numbers = [1, 2, 3]
it = iter(numbers)
print(type(it).__name__)
print(next(it))
print(next(it))
print(next(it))
A fourth next() raises StopIteration.
What for really does
This loop:
for n in [1, 2, 3]:
print(n)
is shorthand for exactly this:
numbers = [1, 2, 3]
it = iter(numbers)
while True:
try:
n = next(it)
except StopIteration:
break
print(n)
for calls iter(), then next() repeatedly, and stops when StopIteration is raised. That's
the whole mechanism — and it's why generators from lesson 9.2 work in for loops for free: a
generator is an iterator.
Iterable but not an iterator
A list can be looped many times because each for gets a fresh iterator:
numbers = [1, 2, 3]
print(list(iter(numbers)) == list(iter(numbers)))
it = iter(numbers)
print(list(it))
print(list(it))
The list is reusable; a single iterator taken from it is not. That's the same one-shot behaviour you saw with generators.
Writing your own iterator
Implement both methods and your class works in a for loop:
class Countdown:
"""Counts down from start to 1."""
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
for n in Countdown(3):
print(n)
Note this class is single-use — self.current is consumed. Returning a fresh iterator from
__iter__ is what makes something reusable.
Usually, just write a generator
The class above is nine lines. A generator does the same in three:
def countdown(start):
while start > 0:
yield start
start -= 1
print(list(countdown(3)))
Generators are iterators — Python writes the __iter__ and __next__ for you. Write the
class only when you need extra state or methods alongside the iteration.
The simplest option of all is often __getitem__ from lesson 5.10, which makes an object
iterable without either protocol method.
Useful iterator tools
itertools is built on this protocol:
import itertools
print(list(itertools.islice(itertools.count(10), 4)))
print(list(itertools.chain([1, 2], [3, 4])))
print([list(g) for _, g in itertools.groupby([1, 1, 2, 2, 3])])
print(list(zip([1, 2, 3], "abc")))
itertools.count() is infinite — islice is what makes taking the first four safe. Infinite
iterators are practical precisely because nothing is computed until requested.
Try it yourself
Checkpoint
- An iterable has
__iter__; an iterator has__next__and raisesStopIterationwhen done. forisiter()plus repeatednext()withStopIterationas the exit.- Lists are reusable iterables; an iterator taken from one is single-use.
- Generators are iterators — prefer them over hand-written iterator classes.
itertoolscomposes iterators, including infinite ones.
Questions about this lesson
Log in to ask a question.