Open In Colab

Iterators

Often an important piece of data analysis is repeating a similar calculation, over and over, in an automated fashion. For example, you may have a table of a names that you'd like to split into first and last, or perhaps of dates that you'd like to convert to some standard format. One of Python's answers to this is the iterator syntax. We've seen this already with the range iterator:

In [ ]:
for i in range(10):
    print(i)
0 1 2 3 4 5 6 7 8 9 

Here we're going to dig a bit deeper. It turns out that in Python 3, range is not a list, but is something called an iterator, and learning how it works is key to understanding a wide class of very useful Python functionality.

Iterating over lists

Iterators are perhaps most easily understood in the concrete case of iterating through a list. Consider the following:

In [ ]:
for value in [2, 4, 6, 8, 10]:
    # do some operation
    print(value + 1)
3
5
7
9
11

The familiar "for x in y" syntax allows us to repeat some operation for each value in the list. The fact that the syntax of the code is so close to its English description ("for [each] value in [the] list") is just one of the syntactic choices that makes Python such an intuitive language to learn and use.

But the face-value behavior is not what's really happening. When you write something like "for val in L", the Python interpreter checks whether it has an iterator interface, which you can check yourself with the built-in iter function:

In [ ]:
iter([2, 4, 6, 8, 10])
Out[ ]:
<list_iterator at 0x104722400>

It is this iterator object that provides the functionality required by the for loop. The iter object is a container that gives you access to the next object for as long as it's valid, which can be seen with the built-in function next:

In [ ]:
I = iter([2, 4, 6, 8, 10])
In [ ]:
print(next(I))
2
In [ ]:
print(next(I))
4
In [ ]:
print(next(I))
6

What is the purpose of this level of indirection? Well, it turns out this is incredibly useful, because it allows Python to treat things as lists that are not actually lists.

range(): A List Is Not Always a List

Perhaps the most common example of this indirect iteration is the range() function in Python 3 (named xrange() in Python 2), which returns not a list, but a special range() object:

In [ ]:
range(10)
Out[ ]:
range(0, 10)

range, like a list, exposes an iterator:

In [ ]:
iter(range(10))
Out[ ]:
<range_iterator at 0x1045a1810>

So Python knows to treat it as if it's a list:

In [ ]:
for i in range(10):
    print(i, end=' ')
0 1 2 3 4 5 6 7 8 9 

The benefit of the iterator indirection is that the full list is never explicitly created! We can see this by doing a range calculation that would overwhelm our system memory if we actually instantiated it (note that in Python 2, range creates a list, so running the following will not lead to good things!):

In [ ]:
N = 10 ** 12
for i in range(N):
    if i >= 10: break
    print(i)
0
1
2
3
4
5
6
7
8
9

If range were to actually create that list of one trillion values, it would occupy tens of terabytes of machine memory: a waste, given the fact that we're ignoring all but the first 10 values!

In fact, there's no reason that iterators ever have to end at all! Python's itertools library contains a count function that acts as an infinite range:

In [ ]:
from itertools import count

for i in count():
    if i >= 10:
        break
    print(i)
0
1
2
3
4
5
6
7
8
9

Had we not thrown-in a loop break here, it would go on happily counting until the process is manually interrupted or killed (using, for example, ctrl-C).

Useful Iterators

This iterator syntax is used nearly universally in Python built-in types as well as the more data science-specific objects we'll explore in later sections. Here we'll cover some of the more useful iterators in the Python language:

enumerate

Often you need to iterate not only the values in an array, but also keep track of the index. You might be tempted to do things this way:

In [ ]:
L = [2, 4, 6, 8, 10]
for i in range(len(L)):
    print(i, L[i])
0 2
1 4
2 6
3 8
4 10

Although this does work, Python provides a cleaner syntax using the enumerate iterator:

In [ ]:
for i, val in enumerate(L):
    print(i, val)
0 2
1 4
2 6
3 8
4 10

This is the more "Pythonic" way to enumerate the indices and values in a list.

zip

Other times, you may have multiple lists that you want to iterate over simultaneously. You could certainly iterate over the index as in the non-Pythonic example we looked at previously, but it is better to use the zip iterator, which zips together iterables:

In [ ]:
L = [2, 4, 6, 8, 10]
R = [3, 6, 9, 12, 15]
for lval, rval in zip(L, R):
    print(lval, rval)
2 3
4 6
6 9
8 12
10 15

Any number of iterables can be zipped together, and if they are different lengths, the shortest will determine the length of the zip.

map and filter

The map iterator takes a function and applies it to the values in an iterator:

In [ ]:
# find the first 10 square numbers
square = lambda x: x ** 2
for val in map(square, range(10)):
    print(val, end=' ')
0 1 4 9 16 25 36 49 64 81 

The filter iterator looks similar, except it only passes-through values for which the filter function evaluates to True:

In [ ]:
# find values up to 10 for which x % 2 is zero
is_even = lambda x: x % 2 == 0
for val in filter(is_even, range(10)):
    print(val, end=' ')
0 2 4 6 8 

The map and filter functions, along with the reduce function (which lives in Python's functools module) are fundamental components of the functional programming style, which, while not a dominant programming style in the Python world, has its outspoken proponents (see, for example, the pytoolz library). We'll have another look at both in the chapter on Data Science.

List Comprehensions

If you read enough Python code, you'll eventually come across the terse and efficient construction known as a list comprehension. This is one feature of Python we expect you will fall in love with if you've not used it before; it looks something like this:

In [ ]:
[i for i in range(20) if i % 3 > 0]
Out[ ]:
[1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19]

The result of this is a list of numbers which excludes multiples of 3. While this example may seem a bit confusing at first, as familiarity with Python grows, reading and writing list comprehensions will become second nature.

Basic List Comprehensions

List comprehensions are simply a way to compress a list-building for-loop into a single short, readable line. For example, here is a loop that constructs a list of the first 12 square integers:

In [ ]:
L = []
for n in range(12):
    L.append(n ** 2)
L
Out[ ]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]

The list comprehension equivalent of this is the following:

In [ ]:
[n ** 2 for n in range(12)]
Out[ ]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]

As with many Python statements, you can almost read-off the meaning of this statement in plain English: "construct a list consisting of the square of n for each n up to 12".

This basic syntax, then, is [expr for var in iterable], where expr is any valid expression, var is a variable name, and iterable is any iterable Python object.

Multiple Iteration

Sometimes you want to build a list not just from one value, but from two. To do this, simply add another for expression in the comprehension:

In [ ]:
[(i, j) for i in range(2) for j in range(3)]
Out[ ]:
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]

Notice that the second for expression acts as the interior index, varying the fastest in the resulting list. This type of construction can be extended to three, four, or more iterators within the comprehension, though at some point code readibility will suffer!

Conditionals on the Iterator

You can further control the iteration by adding a conditional to the end of the expression. In the first example of the section, we iterated over all numbers from 1 to 20, but left-out multiples of 3. Look at this again, and notice the construction:

In [ ]:
[val for val in range(20) if val % 3 > 0]
Out[ ]:
[1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19]

The expression (i % 3 > 0) evaluates to True unless val is divisible by 3. Again, the English language meaning can be immediately read off: "Construct a list of values for each value up to 20, but only if the value is not divisible by 3". Once you are comfortable with it, this is much easier to write – and to understand at a glance – than the equivalent loop syntax:

In [ ]:
L = []
for val in range(20):
    if val % 3:
        L.append(val)
L
Out[ ]:
[1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19]