Learning Python - Mark Lutz [205]
The iteration protocol also is the reason that we’ve had to wrap some results in a list call to see their values all at once. Objects that are iterable return results one at a time, not in a physical list:
>>> R = range(5)
>>> R # Ranges are iterables in 3.0
range(0, 5)
>>> I = iter(R) # Use iteration protocol to produce results
>>> next(I)
0
>>> next(I)
1
>>> list(range(5)) # Or use list to collect all results at once
[0, 1, 2, 3, 4]
Now that you have a better understanding of this protocol, you should be able to see how it explains why the enumerate tool introduced in the prior chapter works the way it does:
>>> E = enumerate('spam') # enumerate is an iterable too
>>> E
>>> I = iter(E) >>> next(I) # Generate results with iteration protocol (0, 's') >>> next(I) # Or use list to force generation to run (1, 'p') >>> list(enumerate('spam')) [(0, 's'), (1, 'p'), (2, 'a'), (3, 'm')] We don’t normally see this machinery because for loops run it for us automatically to step through results. In fact, everything that scans left-to-right in Python employs the iteration protocol in the same way—including the topic of the next section. * * * [33] Terminology in this topic tends to be a bit loose. This text uses the terms “iterable” and “iterator” interchangeably to refer to an object that supports iteration in general. Sometimes the term “iterable” refers to an object that supports iter and “iterator” refers to an object return by iter that supports next(I), but that convention is not universal in either the Python world or this book. [34] Technically speaking, the for loop calls the internal equivalent of I.__next__, instead of the next(I) used here. There is rarely any difference between the two, but as we’ll see in the next section, there are some built-in objects in 3.0 (such as os.popen results) that support the former and not the latter, but may be still be iterated across in for loops. Your manual iterations can generally use either call scheme. If you care for the full story, in 3.0 os.popen results have been reimplemented with the subprocess module and a wrapper class, whose __getattr__ method is no longer called in 3.0 for implicit __next__ fetches made by the next built-in, but is called for explicit fetches by name—a 3.0 change issue we’ll confront in Chapters 37 and 38, which apparently burns some standard library code too! Also in 3.0, the related 2.6 calls os.popen2/3/4 are no longer available; use subprocess.Popen with appropriate arguments instead (see the Python 3.0 library manual for the new required code). List Comprehensions: A First Look Now that we’ve seen how the iteration protocol works, let’s turn to a very common use case. Together with for loops, list comprehensions are one of the most prominent contexts in which the iteration protocol is applied. In the previous chapter, we learned how to use range to change a list as we step across it: >>> L = [1, 2, 3, 4, 5] >>> for i in range(len(L)): ... L[i] += 10 ... >>> L [11, 12, 13, 14, 15] This works, but as I mentioned there, it may not be the optimal “best-practice” approach in Python. Today, the list comprehension expression makes many such prior use cases obsolete. Here, for example, we can replace the loop with a single expression that produces the desired result list: >>> L = [x + 10 for x in L] >>> L [21, 22, 23, 24, 25] The net result is the same, but it requires less coding on our part and is likely to run substantially faster. The list comprehension isn’t exactly the same as the for loop statement version because it makes a new list object (which might matter if there are multiple references to the original list), but it’s close enough for most applications and is a common and convenient enough approach to merit a closer look here. List Comprehension Basics We met the list comprehension