Learning Python - Mark Lutz [391]
>>> X = Squares(1, 5) # Iterate manually: what loops do
>>> I = iter(X) # iter calls __iter__
>>> next(I) # next calls __next__
1
>>> next(I)
4
...more omitted...
>>> next(I)
25
>>> next(I) # Can catch this in try statement
StopIteration
An equivalent coding of this iterator with __getitem__ might be less natural, because the for would then iterate through all offsets zero and higher; the offsets passed in would be only indirectly related to the range of values produced (0..N would need to map to start..stop). Because __iter__ objects retain explicitly managed state between next calls, they can be more general than __getitem__.
On the other hand, using iterators based on __iter__ can sometimes be more complex and less convenient than using __getitem__. They are really designed for iteration, not random indexing—in fact, they don’t overload the indexing expression at all:
>>> X = Squares(1, 5)
>>> X[1]
AttributeError: Squares instance has no attribute '__getitem__'
The __iter__ scheme is also the implementation for all the other iteration contexts we saw in action for __getitem__ (membership tests, type constructors, sequence assignment, and so on). However, unlike our prior __getitem__ example, we also need to be aware that a class’s __iter__ may be designed for a single traversal, not many. For example, the Squares class is a one-shot iteration; once you’ve iterated over an instance of that class, it’s empty. You need to make a new iterator object for each new iteration:
>>> X = Squares(1, 5)
>>> [n for n in X] # Exhausts items
[1, 4, 9, 16, 25]
>>> [n for n in X] # Now it's empty
[]
>>> [n for n in Squares(1, 5)] # Make a new iterator object
[1, 4, 9, 16, 25]
>>> list(Squares(1, 3))
[1, 4, 9]
Notice that this example would probably be simpler if it were coded with generator functions or expressions (topics introduced in Chapter 20 and related to iterators):
>>> def gsquares(start, stop):
... for i in range(start, stop+1):
... yield i ** 2
...
>>> for i in gsquares(1, 5): # or: (x ** 2 for x in range(1, 5))
... print(i, end=' ')
...
1 4 9 16 25
Unlike the class, the function automatically saves its state between iterations. Of course, for this artificial example, you could in fact skip both techniques and simply use a for loop, map, or a list comprehension to build the list all at once. The best and fastest way to accomplish a task in Python is often also the simplest:
>>> [x ** 2 for x in range(1, 6)]
[1, 4, 9, 16, 25]
However, classes may be better at modeling more complex iterations, especially when they can benefit from state information and inheritance hierarchies. The next section explores one such use case.
Multiple Iterators on One Object
Earlier, I mentioned that the iterator object may be defined as a separate class with its own state information to support multiple active iterations over the same data. Consider what happens when we step across a built-in type like a string:
>>> S = 'ace'
>>> for x in S:
... for y in S:
... print(x + y, end=' ')
...
aa ac ae ca cc ce ea ec ee
Here, the outer loop grabs an iterator from the string by calling iter, and each nested loop does the same to get an independent iterator. Because each active iterator has its own state information, each loop can maintain its own position in the string, regardless of any other active loops.
We saw related examples earlier, in Chapters 14 and 20. For instance, generator functions and expressions, as well as built-ins like map and zip, proved to be single-iterator objects; by contrast, the range built-in and other built-in types, like lists, support multiple active iterators with independent positions.
When we code