Iterators
An iterator is nothing more than a container object that implements the iterator protocol. This protocol consists of two methods:
- __next__: This returns the next item of the container
- __iter__: This returns the iterator itself
Iterators can be created from a sequence using the iter built-in function. Consider the following example:
>>> i = iter('abc') >>> next(i) 'a' >>> next(i) 'b' >>> next(i) 'c' >>> next(i) Traceback (most recent call last): File "<input>", line 1, in <module> StopIteration
When the sequence is exhausted, a StopIteration exception is raised. It makes iterators compatible with loops, since they catch this exception as a signal to end the iteration. If you create a custom iterator, you need to provide objects with the implementation of __next__, which iterates the state of the object, and the __iter__ method, which returns the iterable.
Both methods are often implemented inside of the same class. The following is an example of the CountDown class, which allows us to iterate numbers toward 0:
class CountDown:
def __init__(self, step): self.step = step
def __next__(self): """Return the next element.""" if self.step <= 0: raise StopIteration self.step -= 1 return self.step
def __iter__(self): """Return the iterator itself.""" return self
The preceding class implementation allows it to iterate over itself. This means that once you iterate over its content, the iterable is exhausted and cannot be iterated anymore:
>>> count_down = CountDown(4)
>>> for element in count_down:
... print(element)
... else:
... print("end")
...
3
2
1
0
end
>>> for element in count_down:
... print(element)
... else:
... print("end")
...
end
If you want your iterator to be reusable, you can always split its implementation into two classes in order to separate the iteration state and actual iterator objects, as in the following example:
class CounterState:
def __init__(self, step):
self.step = step
def __next__(self): """Move the counter step towards 0 by 1.""" if self.step <= 0: raise StopIteration self.step -= 1 return self.step
class CountDown:
def __init__(self, steps): self.steps = steps
def __iter__(self):
"""Return iterable state"""
return CounterState(self.steps)
If you separate your iterator from its state, you will ensure that it can't be exhausted:
>>> count_down = CountDown(4)
>>> for element in count_down:
... print(element)
... else:
... print("end")
...
3
2
1
0
end
>>> for element in count_down:
... print(element)
... else:
... print("end")
...
3
2
1
0
end
Iterators themselves are a low-level feature and concept, and a program can live without them. However, they provide the base for a much more interesting feature: generators.