login
Holden Web
What you'll need to know tomorrow

Iteration in Python

Iterables vs. Iterators

In the broadest possible terms, an iterable is something you can iterate over, and an iterator is what the interpreter uses to do the iteration. This description, however, is too general to be of enough value. The fundamental question is: what does the interpreter do when you write for i in s: in your program or other module?

In modern Python, iteration is supported by two quite separate mechanisms. So the answer to the question "how does the interpreter iterate over objects?" depends on the presence of specific methods on the object. If the object has an __iter__() method, then it is iterable using the new-style iteration mechanism. Otherwise the interpreter looks for a __getitem__() method and, if it finds one, uses the old-style iteration mechanism. If neither method is present, the interpreter raises a TypeError exception because the object is not iterable.

Old-Style Iteration

If an object, o, has no __iter__() method and you tell the interpreter to iterate over it, the interpreter initializes an internal variable to zero and repeatedly calls the object's __getitem__() method with successively higher values of the internal variable. From the point of view of the object, it's as though it were being manipulated by this code:

OBSERVE: Effective logic of an old-style for loop
# Approximate equivalent of:
#    for val in o:
#        # [loop body]
intern = 0
while True:
    try:
        val = o[intern]
    except IndexError:
        break
    # [loop body]
    intern += 1

In fact, you can create your own classes whose instances can be iterated over in this way. All you need to do is provide a __getitem__(n) method that raises an IndexError exception when the value of n is too high. Suppose you wanted to implement fixed-length sequences of objects. You could define a function to create an appropriate sequence (list or tuple or string) with the required number of components in it (so fls("*", 12) would return "************", for example).

Alternatively, you could define an fls class, whose __init__() method had the same signature as the function above. Create fls.py as shown.

Code
"""
Simple demonstration of the "old iteration protocol" - still available.
"""

class fls(object):

    def __init__(self, val, times):
        self.val = val
        self.count = times

    def __getitem__(self, n):
        if n >= self.count:
            raise IndexError("Object has no item %s %n")
        return self.val

thing = fls("*", 5)
for c in thing:
    print(c)

thing = fls(120,3)
for c in thing:
    print(c)

Save and run it. You see the following output:

OBSERVE: Output from running the fls class
*
*
*
*
*
120
120
120

So, for iteration purposes, you can see that the fls objects appear to act like other sequences, only with very boring behavior because all elements are constrained to be the same—the only value that __getitem__() ever returns is the one that was passed in to __init__(). But the main point is that you know a little more about Python's iteration mechanism. Now try a few other cases for yourself—use an interactive console session to create and test out some further fls objects interactively.

NoteRemember you will need to import the fls class from the fls module in order to be able to create instances of it.
New-Style Iteration

The iteration mechanism outlined above is all very well when you are iterating over numbered items in a sequence, but it does not naturally extend to collections like sets and dicts, which do not specify a natural ordering for their items. Dicts, in fact, do have a __getitem__() method, but it takes a key value and returns the appropriate item (assuming that a key with that value exists—if there is no such key, it raises a KeyError exception). Sets don't even have a __getitem__() method, since they are effectively "item-less dicts".

It was to overcome issues like this that the "new-style" iteration protocol was defined. You learned above that the interpreter will look for an __iter__() method on the objects that you iterate over. If it finds __iter__(), it uses it to create an iterator from the iterable you are iterating over.

The iterator will have a __next__() method—this is a requirement of the iteration protocol. Each time around the loop, the interpreter obtains the next value for the iterable by calling the iterator's __next__() method. Again, you can perhaps understand this more easily with an approximate Python equivalent to a for-loop over a new-style iterable:

OBSERVE: Effective logic of a new-style for loop
# Approximate equivalent of:
#    for val in o:
#        # [loop body]
it = o.__iter__()
while True:
    try:
        val = it.__next__()
    except StopIteration:
        break
    # [loop body]

You may wonder why Python insists on creating a new object for each iteration: couldn't it just use the iterable directly? The answer to that question is "no": the iterator contains the current state of the iteration, and code that iterates over the same iterable twice is perfectly legal. Iterating over the same iterator, however, gives results that are not usually what you want. You can see this by playing with the interactive interpreter.

Code and output
>>> lst = [1, 2, 3]
>>> dir(lst)
[..., '__getitem__', ..., '__init__', ...]
>>> for i in lst:
...     for j in lst:
...         print(i, j)
...
1 1
1 2
1 3
2 1
2 2
2 3
3 1
3 2
3 3
>>> li = lst.__iter__()
>>> dir(li)
[..., '__iter__', ..., '__next__', ...]
>>> for i in li:
...     for j in li:
...         print(i, j)
...
1 2
1 3
>>> lii = li.__iter__()
>>> li
<list_iterator object at 0x...>
>>> lii
<list_iterator object at 0x...>
>>> l2 = lst.__iter__()
>>> l2.__next__()
1
>>> l2.__next__()
2
>>> l2.__next__()
3
>>> l2.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>
Modern Python The repr() of a list_iterator includes a memory address (e.g. <list_iterator object at 0x102e1c130>). The address varies between runs; 0x... is used above as a placeholder, as it is throughout this lesson wherever object addresses appear.

What's the difference between the first for loop and the second? Look at the dir() listing of lst, which is a list instance, and note that it has both __iter__() and __getitem__() methods. When the interpreter iterates over the list, calling its __iter__() method creates a new iterator, yielding a complete sequence of values, every time it encounters a for loop.

We then created a list iterator object by manually calling our list's __iter__() method. Note that the list iterator object also has an __iter__() method, and adds a __next__() method, but it lacks a __getitem__(). The __iter__() method of the iterator is rather different from that of the list, however:

OBSERVE: __iter__() method - in Python it would read:
def __iter__(self):
    return self

In other words, each time you iterate over a list (which is an iterable), the call to its __iter__() method creates a new iterator, which has its own independent state. The iterator's __iter__() method, however, does not create a new iterator, which means that the inner and outer loops are sharing the same iterator. This in turn means that by the time the outer loop is trying to begin its second iteration, the iterator has already been exhausted by the inner loop and (for the second time) raises the StopIteration exception.

The final few statements demonstrated this by manually going through the steps that the interpreter does when iterating over a list. We saw the l2 iterator produce three values on successive __next__() calls before raising a StopIteration exception. Normally, of course, the exception is caught internally by the logic of the for loop, and therefore does not become visible.

In summary, calling an iterable's __iter__() method creates an iterator that can be used to iterate over the iterable.

Creating Your Own Iterators

Now that you understand Python's iteration processes somewhat better, you may be wondering whether you can define your own iterable classes. The answer is "yes"! You will need to provide an __iter__() method (which can simply return self if you are implementing an iterator rather than a more general iterable: this is usually OK, since when you write an iterator class it is easy to create multiple instances, each having independent state). The __next__() method should return successive values until there are no more, at which point it should raise a StopIteration exception.

Rather than create an example now, we'll create it in the next section. First, we'll create a generator, and then we'll build an equivalent iterator.

Generators: Avoiding Creation of Large Sequences

The iteration protocol discussed above also comes into play with so-called generator functions. The only apparent difference between a generator function and the regular kind you have dealt with before is the appearance of the yield keyword in the function body. So what's the difference between a regular function and a generator function?

The answer is that calling a generator function produces a special type of iterator object (a "generator"). The function namespace is created and initialized with the argument values. The function code only starts executing with the first call to the generator's __next__() method. Execution continues until a yield expression is evaluated: the value of the expression following yield becomes the value of the __next__() method call. You can see this with a very simple generator function in an interactive session.

Code and output
>>> def g(x):
...      yield x
...      x *= 2
...      yield x
...
>>> g
<function g at 0x...>
>>> gen = g("##")
>>> gen
<generator object g at 0x...>
>>> dir(gen)
['__class__', ..., '__iter__', ..., '__next__', ...]
>>> gen.__next__()
'##'
>>> gen.__next__()
'####'
>>> gen.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> gen.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

g() is a generator function, though when you ask the interpreter about it you don't see any difference from any other function. Calling it creates a generator object, though, and the dir() listing shows that it has the necessary methods for an iterator. Calling the object's __next__() method returns the result of the next yield expression in the function's code body.

If the function ends before encountering a yield expression (either by executing a return statement or dropping off the bottom), the __next__() method call raises a StopIteration exception just like any other iterator. Also note that, once the generator starts to raise StopIteration exceptions when __next__() is called, it continues to do so for each subsequent call—the iterator is exhausted.

Advantages of Generator Functions

The really convenient thing about generator functions is that they allow you to perform all sorts of complex calculations to produce the values in a sequence, but the code that consumes (makes use of) these values can be entirely separated from the generator that produces them. The values are consumed in a simple for loop—or any other similar iterative context in Python, such as a list comprehension.

Not only do they make your code simpler by separating out the production and consumption of sequences, but generators allow you to create sequence values one at a time, as they are consumed. There is no need to build a list or tuple to store them in, which means your programs will use less storage and operate more quickly (though these advantages do not really make much difference unless the number of objects becomes large).

A Simple Generator Function

Suppose you need to produce sequences determined by a list, but need to repeat the first list element once, the second twice, and so on. So given a list [2, 4, 6], the resulting sequence would be 2, 4, 4, 6, 6, 6. Let's write a generator that produces such sequences. First, though, we'll write tests to ensure that our generator function works. Create testgen.py as shown:

Code
"""
testgen.py: simple test for a sequence generator
"""
import unittest
from gen123 import gen123

class TestGen(unittest.TestCase):

    def testEmpty(self):
        self.assertEqual(list(gen123([])), [], "Empty list does not give empty list")

    def test123(self):
        self.assertEqual(list(gen123([1])), [1], "[1] does not give [1]")
        self.assertEqual(list(gen123([1, 2])), [1, 2, 2])
        self.assertEqual(list(gen123([1, 2, 3])), [1, 2, 2, 3, 3, 3])

if __name__ == "__main__":
    unittest.main()

As usual, we start out with a simple stub function to make sure that the tests fail. Now, create gen123.py as shown:

Code
"""
gen123.py: generate sequences from a base list, repeating
           each element one more time than the last
"""

def gen123(m):
    yield None

Save and run the test program:

OBSERVE: Results from running testgen.py
FF
======================================================================
FAIL: test123 (__main__.TestGen)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "testgen.py", line 13, in test123
    self.assertEqual(list(gen123([1])), [1], "[1] does not give [1]")
AssertionError: [1] does not give [1]

======================================================================
FAIL: testEmpty (__main__.TestGen)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "testgen.py", line 10, in testEmpty
    self.assertEqual(list(gen123([])), [], "Empty list does not give empty list")
AssertionError: Empty list does not give empty list

----------------------------------------------------------------------
Ran 2 tests in 0.032s

FAILED (failures=2)

Now, let's see how it does with some real code in there.

Code

"""
gen123.py: generate sequences from a base list, repeating
           each element one more time than the last
"""

def gen123(m):
    yield None
    
    n = 0
    for item in m:
        n += 1
        for i in range(n):
            yield item
Modern Python This generator function is a fine idiomatic solution. An equivalent one-liner using itertools would be: from itertools import chain, repeat; chain.from_iterable(repeat(v, i+1) for i, v in enumerate(m)). For the purposes of this lesson the hand-rolled generator is clearer.

Save and run the test program:

OBSERVE: Output from testgen.py; the tests now pass
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
An Iterator Equivalent of the Generator

As you learned above, it is also possible to write classes that obey the iteration protocol. You will end this lesson by writing an iterator equivalent of the generator function above. Since you want it to perform exactly the same as the gen123 generators, you can use the same tests to verify its operation—that is one of the benefits of a test-driven environment! The new component should ideally be a "drop-in replacement" for the generator function. Create class123.py as shown:

Code
"""
A simple iterator object specification.
"""

class gen123:

    def __init__(self, lst):
        "Initialize the iterator object."
        self.lst = lst
        self.itemno = 0
        self.count = 1

    def __iter__(self):
        "This object is not an iterable."
        return self

    def __next__(self):
        "Return the next value in the output sequence."
        if self.count > self.itemno:
            try:
                self.val = self.lst[self.itemno]
            except IndexError:
                raise StopIteration
            self.itemno += 1
            self.count = 1
        self.count += 1
        return self.val
Modern Python Compare the iterator class above with the gen123 generator function in the previous section. The generator version is roughly half the length and considerably easier to follow. When you need a custom iterator, a generator function (or generator expression) is almost always the simpler choice; a full iterator class is worth writing only when you need fine-grained control over iteration state that a generator cannot easily provide.

This code is considerably more complex. This should not be surprising, because generator functions were devised to solve this type of problem cleanly and simply.

Instead of calling the generator function, the test routine will now call your iterator's class (which, you will notice, has the same name). This causes its __init__() method to be run, and the list of values is stored as an instance variable. Two other instance variables are initialized: one to keep track of which item is currently being output, and the other to keep track of how many times the current value has been produced.

All the magic, of course, takes place in the __next__() method. First it checks to see whether it is time to move to the next element of the value list (the item number and count are set up initially to ensure that this branch is actioned on the first call). If so, the val instance variable is retrieved.

If no more values are available, the method raises a StopIteration exception to terminate the loop. Note carefully that this action can be repeated—once the method starts to raise the exception, it should be raised for every subsequent call.

Once the correct item value is established, the count is incremented and the value is returned as the result of the call.

This code is about twice as long as that of the generator solution, and so you would probably choose to write a generator function for problems like this. But if you need close control over iterative behavior, you may end up needing to write your own iterators.

Testing the module is easy. Just make the following change to the test program:

Code
"""
testgen.py: simple test for a list generator function
"""
import unittest
from class123 import gen123

class TestGen(unittest.TestCase):

    def testEmpty(self):
        self.assertEqual(list(gen123([])), [], "Empty list does not give empty list")

    def test123(self):
        self.assertEqual(list(gen123([1])), [1], "[1] does not give [1]")
        self.assertEqual(list(gen123([1, 2])), [1, 2, 2])
        self.assertEqual(list(gen123([1, 2, 3])), [1, 2, 2, 3, 3, 3])

if __name__ == "__main__":
    unittest.main()

Save and run the updated test program; you should see a successful result immediately, thereby giving strong evidence that the two implementations are equivalent.

Generator Expressions

After the new-style iteration protocol was adopted in Python, one of the developers observed that it would be very useful to be able to write expressions that were similar to list comprehensions in using iteration (for) and selection (if) elements to produce expressions that generated their results rather than producing a list. The reasoning behind this is just the same as the reasoning behind standard generators—creating the objects one by one "on demand" is more space-efficient, and is likely to speed up programs dealing with large sequences considerably, as well as reducing their memory requirements.

The syntax of a generator expression is the same as for list comprehensions (learned in an earlier course), but with parentheses instead of brackets. Because they are generators, however, you only see the individual values when you consume them inside an iteration. Learn a little more about them by playing in an interactive interpreter session.

Code and output
>>> gx1 = (x for x in range(10) if x % 3)
>>> gx1
<generator object <genexpr> at 0x...>
>>> list(gx1)
[1, 2, 4, 5, 7, 8]
>>> list(gx1)
[]
>>> sum(i for i in range(100))
4950
>>> gx2 = (ord(c) for c in "Jim")
>>> next(gx2)
74
>>> next(gx2)
105
>>> next(gx2)
109
>>> next(gx2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>
Modern Python Generator expressions are idiomatic Python and should be your first choice when building a sequence lazily. They compose naturally with built-in aggregates: sum(x*x for x in data), max(len(s) for s in words), and so on. For more powerful lazy pipelines, the standard library's itertools module (especially islice, chain, takewhile, and count) is worth exploring.

Note that the generator expressions are iterators, but not iterables: once you have iterated over them they are exhausted, and any further attempt to iterate over the expression raises an immediate StopIteration. Also observe that you used a new built-in function in that session. Calling next(o) is pretty much equivalent to calling o.__next__(), right down to the raising of a StopIteration execution when no more values are available.

Generators and generator expressions primarily offer memory savings, though this can equate to time savings if you are avoiding a lot of memory allocation and deallocation. For very large data sets, it can make a computation practical that you might otherwise not have enough memory for.

You now know much more about the way Python iterates over objects than you formerly did. With luck, this knowledge will allow you to build objects that help you solve your problems more effectively.