Generators allow sequences of values to be handled while computing each value of the sequence only as it is needed, rather than as a traditional list (which must compute all of its values ahead of time).
Using generators where appropriate can result in substantial memory savings, because large collections of data do not need to be stored in memory in their entirety. Similarly, generators are uniquely able to handle representation of some sequences that cannot be accurately represented by lists.
This chapter explains what a generator is, and the syntax for using generators in Python. It also covers some of the common generators that are provided in the Python standard library.
A generator is a function that, instead of executing and returning a single value, sends back one or more values in a sequence. A generator function executes until it is told to yield a value, and then it continues execution until told to do so again. This continues until the function is complete, or until iteration over that generator terminates.
There is no explicit requirement that a generator terminate at all; generators may represent infinite sequences. There is nothing inherently wrong with this. In cases where this occurs, it is simply the responsibility of the code iterating over the generator to break out of the sequence when appropriate (such as with a break statement).
A generator function is recognizable by the presence of one or more yield statements inside the function, usually instead of a return statement. In Python 2, a yield statement and a return statement cannot coexist in the same function. However, in Python 3, it is possible to have both yield and return (discussed in more detail later).
Like the return statement, the yield statement commands the function to send back a value to the caller. Unlike the return statement, however, the yield statement does not actually terminate the function's execution. Rather, execution is temporarily halted until the generator is resumed by the calling code, at which point it picks up where it left off.
Consider the following very simple generator:
def fibonacci():
yield 1
yield 1
yield 2
yield 3
yield 5
yield 8
This generator represents the beginning of the Fibonacci sequence (that is, the sequence in which each integer is the sum of the previous two). You can iterate generators, as you can see by using a simple for…in loop in the Python interactive terminal.
>>> for i in fibonacci():
... print(i)
...
1
1
2
3
5
8
Obviously, this particular generator is probably better represented as a plain Python list. However, consider a generator which, instead of returning six Fibonacci numbers, returns an infinite series of them, as shown here:
def fibonacci():
numbers = []
while True:
if len(numbers) < 2:
numbers.append(1)
else:
numbers.append(sum(numbers))
numbers.pop(0)
yield numbers[-1]
This generator will yield an infinite sequence of Fibonacci numbers. Using the simple for…in from the interactive terminal shown previously would simply print numbers, which very quickly become humorously long (that is, to the screen into perpetuity).
Unlike the previous fibonacci function, this one is not better represented as a simple Python list. In fact, not only would it be unwise to try to represent this as a simple Python list, it would be impossible. Python lists cannot store infinite sequences of values.
You can ask a generator for a value without using a for…in loop. Sometimes you may want to just get a single value, or a fixed number of values. Python provides the built-in next function, which can ask a generator (actually, any object with a _next_ method, called next in Python 2) for its next value.
The earlier fibonacci function yields an infinite sequence of Fibonacci numbers. Instead of iterating over the entire thing, you can ask for values one at a time.
First, you simply create your generator by calling the fibonacci function and saving its returned value. Because the function has yield statements rather than a return statement, the Python interpreter knows to just return the generator object.
>>> gen = fibonacci()
>>> gen
<generator object fibonacci at 0x101555dc8>
At this point, it is worth noting that none of the code within fibonacci has actually run. The only thing that the interpreter has done is recognize that a generator is present and return a generator object, which is ready to run the code once a value is requested.
You can use the built-in next function to request your first value, as shown here:
>>> next(gen)
1
Now (and only now) some of the actual code in the fibonacci function has been run. (To make the explanation as clear as possible, an explicit continue statement has been added at the end of the loop.)
def fibonacci():
numbers = []
while True:
if len(numbers) < 2: # True; numbers == []
numbers.append(1)
else:
numbers.append(sum(numbers))
numbers.pop(0)
yield numbers[-1]
continue
The function is entered, and it begins the first iteration of the while loop. Because the numbers list is empty at this point, the value 1 is appended to the list. Finally, you get to the yield numbers[-1] statement. At this point, the generator has been given a value to yield, so execution halts, and the value 1 is yielded. This is where the execution ends; the continue statement does not yet run.
Now, issue next(gen) again, as shown here:
>>> next(gen)
1
Execution picks up where it left off, which means the first thing to run is the continue statement.
def fibonacci():
numbers = []
while True:
if len(numbers) < 2:
numbers.append(1)
else:
numbers.append(sum(numbers))
numbers.pop(0)
yield numbers[-1]
continue
This sends you back to the top of the while loop. Your numbers list only has one member (it is [1]), so len(numbers) is still less than 2, and that path is chosen at the if statement again. Your numbers list is now [1, 1], and the final element of the list is yielded, stopping execution.
def fibonacci():
numbers = []
while True:
if len(numbers) < 2: # True; numbers == [1]
numbers.append(1)
else:
numbers.append(sum(numbers))
numbers.pop(0)
yield numbers[-1]
continue
Now, issue next(gen) yet again, as shown here:
>>> next(gen)
2
Again, execution picks up where it left off, meaning the next thing to run is the continue statement.
def fibonacci():
numbers = []
while True:
if len(numbers) < 2:
numbers.append(1)
else:
numbers.append(sum(numbers))
numbers.pop(0)
yield numbers[-1]
continue
The continue statement sends the interpreter back to the stop of the while loop. However, now it takes the else pathway when it gets to the if statement, because numbers is now a list with two elements ([1, 1]). The sum of the two elements is then appended to the end of the list, and the first element is removed. Again, you get to the yield statement, and it yields the final element of the list, which is 2.
def fibonacci():
numbers = []
while True:
if len(numbers) < 2: # False; numbers == [1, 1]
numbers.append(1)
else:
numbers.append(sum(numbers))
numbers.pop(0)
yield numbers[-1]
continue
If you issue next(gen) again, the interpreter will follow the same path (because the length of the numbers list is still 2). Of course, now the numbers list itself has changed from [1, 1] to [1, 2], so the result is different. The value 3 is appended to the list, the 1 is lopped off of the beginning, and 3 is yielded.
>>> next(gen)
3
If you continue to ask for more values, you see this pattern repeat. The same code runs, but against an updated numbers list, so the yielded values continue along the Fibonacci series.
>>> next(gen)
5
>>> next(gen)
8
>>> next(gen)
13
>>> next(gen)
21
Notice that a few things are not happening. You are not storing a huge list of Fibonacci numbers in memory. The only numbers that you must store are the most recent two, because they are required to find the next number in the series. The generator scraps anything that is out of date. This would matter if the generator were to continue on indefinitely, because if you needlessly held on to every previous value, eventually the list would fill up free memory.
Similarly, the generator only computes each value in the series when it is specifically requested. At this point in code execution, the generator has not bothered to determine that the next value that it will need to yield back (if asked) is 34, precisely because it may not be asked.
As with other functions, with generators, you may want to have more than one potential exit path. For example, the following “plain” function has multiple exit paths using multiple return statements:
def my_function(foo, add_extra_things=True):
foo += '\nadded things'
if not add_extra_things:
return foo
foo += '\n added extra things'
return foo
This function normally returns at the end of the block. However, if the keyword argument add_extra_things is provided and set to False, the earlier return statement on the third line of the function will be hit instead, and function execution will be cut off there.
Plenty of reasons exist to do this, and generators must have a mechanism to serve a similar purpose.
The correct approach for this depends somewhat on which version of Python you are using. In Python 2, generators are not allowed to have return statements. If you attempt to write a function with both a yield statement and a return statement, you get a syntax error, as shown here:
>>> def my_generator():
... yield 1
... return
...
File "<stdin>", line 3
SyntaxError: 'return' with argument inside generator
Instead, Python provides a built-in exception called StopIteration, which serves a similar purpose. When a generator is being iterated over and StopIteration is raised, this signals that the generator's iteration is complete, and it exits. The exception is caught in this case, and there is no traceback. On the other hand, if next is being used, the StopIteration exception bubbles.
Consider the following simple generator:
>>> def my_generator():
... yield 1
... yield 2
... raise StopIteration
... yield 3
If you iterate over this, you will get the values 1 and 2, and then the generator will exit cleanly. The yield 3 statement never runs (similar to code that exists after a return statement).
>>> [i for i in my_generator()]
[1, 2]
If you manually run next on the generator, the first two next calls will yield values, and the third (and any subsequent) call will raise a StopIteration exception, as shown here:
>>> gen = my_generator()
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in my_generator
StopIteration
In Python 3, the situation is similar, but you have one additional syntactic option. Python 3 removes the restriction that yield and return cannot appear together in a function. In this case, using return effectively becomes an alias for raise StopIteration.
It is worth noting that if you return a value in your return statement, it does not become a final yielded value. Rather, the value is sent as the exception message. Consider the following statement:
return 42
This is equivalent to the following:
raise StopIteration(42)
And, very importantly, it is not equivalent to the following:
yield 42
return
In code that is intended to be cross-compatible with Python 2 and Python 3, it is probably preferable to use the raise StopIteration form explicitly. In code that only runs on Python 3, it likely does not matter much.
The generators explored thus far are unidirectional in their communication. They yield values to the calling code; nothing is ever sent to the generator.
However, the generator protocol also supports an additional send method that allows communication back to a generator. This works because the yield statement is actually an expression. In addition to yielding back its value, if a generator is resumed with send rather than next, the value provided to send can actually be assigned to the result of the yield expression.
Consider the following generator to return the perfect squares in order. This is trivial.
def squares():
cursor = 1
while True:
yield cursor ** 2
cursor += 1
However, you may want to tell the generator to move to a certain point, forward or backward. You could implement that capability with a small change to your generator code, as shown here:
def squares(cursor=1):
while True:
response = yield cursor ** 2
if response:
cursor = int(response)
else:
cursor += 1
Now you are assigning the result of the yield expression to the response variable (if and only if there is a result—you do not want to plow over your value with None).
This enables you to jump around within the squares generator, as shown here:
>>> sq = squares()
>>> next(sq)
1
>>> next(sq)
4
>>> sq.send(7)
49
>>> next(sq)
64
What has happened here? First, the interpreter entered the generator and was asked to yield two values (1 and 4). But, the next time, the generator was sent the value 7. The squares generator is coded such that if a value is sent back, the cursor variable is set to that value. So, instead of cursor being incremented to 3, it is set to 7.
The generator then continues as before. The interpreter goes back to the top of the while loop. Because cursor is now 7, the value yielded is 49 (72). This generator is written such that it simply continues from there, so when next is called against it again, cursor increments as before, to 8, and the next value to be yielded is 64 (82).
It is entirely up to the generator to determine how (and whether) sent values are handled. The generators previously explored in this chapter simply ignore them. A generator could, by contrast, use the sent cursor value as a one-off, and then return to its previous spot, as shown here:
def squares(cursor=1):
response = None
while True:
if response:
response = yield response ** 2
continue
response = yield cursor ** 2
cursor += 1
This version of the squares generator does exactly that:
>>> sq = squares()
>>> next(sq)
1
>>> next(sq)
4
>>> sq.send(7)
49
>>> next(sq)
9
The difference here is entirely in the behavior of the generator. There is no magic for how send behaves. The purpose of send is to provide a mechanism for two-way communication with a generator. It is the responsibility of the generator to determine whether (and how) it handles values sent to it.
Generators in Python are a kind of iterator. An iterator in Python is any object that has a __next__ method (and, therefore, is able to respond to the next function).
This is distinct from an iterable, which is any object that defines an __iter__ method. An iterable object's __iter__ method is responsible for returning an iterator.
For an example of the subtle distinction here, consider the Python 3 range function (known as xrange in Python 2). It is commonly believed that range objects are, in fact, generators. However, they are not, as shown here:
>>> r = range(0, 5)
>>> r
range(0, 5)
>>> next(r)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'range' object is not an iterator
This is confusing to many, because an idiom such as for i in range(0, 5) is often one of the first things that you learn in Python. This works because the range function returns an iterable.
The actual iterator that the range object's __iter__ method returns, however, is a generator, and responds as expected to the next method.
>>> r = range(0, 5)
>>> iterator = iter(r)
>>> iterator
<range_iterator object at 0x10055ecc0>
>>> next(iterator)
0
>>> next(iterator)
1
Also, as you would expect, calling next after the generator has finished yielding values will raise StopIteration.
>>> next(iterator)
2
>>> next(iterator)
3
>>> next(iterator)
4
>>> next(iterator)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
When thinking about generators, remember that generators are iterators, but they are not necessarily iterables. Similarly, not all iterables are iterators.
The Python standard library includes several generators, which you may already use, possibly without even realizing that they are generators.
During the earlier discussion about the distinction between iterables and iterators, you learned about the range function, which returns an iterable range object.
The range object's iterator is a generator. It returns sequential values, beginning with the range object's floor, and continuing through its ceiling. By default, its sequence is simply adding one to each value to get the next value to yield. But an optional third argument to the range function, step, enables you to specify a different increment, including a negative one.
The built-in dictionary class in Python includes three methods that allow for iterating over the dictionary, and all three are iterables whose iterators are generators: keys, values, and items.
The purpose of these methods is to allow for iteration over the keys, values, or two-tuples of keys and values (items) of a dictionary, as shown here:
>>> dictionary = {'foo': 'bar', 'baz': 'bacon'}
>>> iterator = iter(dictionary.items())
>>> next(iterator)
('foo', 'bar')
>>> next(iterator)
('baz', 'bacon')
One value of using a generator here is that it prevents the need to make an additional copy of the dictionary (or pieces of the dictionary) in another format. dict.items does not need to reformat the entire dictionary into a list of two-tuples. It simply returns back one two-tuple at a time, when it is requested.
You can see a side effect of this if you attempt to alter the dictionary during iteration, as shown here:
>>> dictionary = {'foo': 'bar', 'baz': 'bacon'}
>>> iterator = iter(dictionary.items())
>>> next(iterator)
('foo', 'bar')
>>> dictionary['spam'] = 'eggs'
>>> next(iterator)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration
Because the items iterator is a generator that simply reads from the referenced dictionary, it does not know what it should do if the dictionary changes while it is working. In the face of ambiguity, it refuses the temptation to guess, and raises RuntimeError instead.
Python includes a built-in function called zip that takes multiple iterable objects and iterates over them together, yielding the first element from each iterable (in a tuple), then the second, then the third, and so on, until the end of the shortest iterable is reached. Following is an example:
>>> z = zip(['a', 'b', 'c', 'd'], ['x', 'y', 'z'])
>>> next(z)
('a', 'x')
>>> next(z)
('b', 'y')
>>> next(z)
('c', 'z')
>>> next(z)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
The reasons to use zip are similar to the reasons to use dict.items and family. Its purpose is to yield back members of its iterables in a different structure, one set at a time. This alleviates the need to copy over the entire thing in memory if such an operation is not necessary.
A cousin to the zip function is the built-in map function. The map function takes a function that accepts N arguments as well as N iterables, and computes the result of the function against the sequential members of each iterable, stopping when it reaches the end of the shortest one.
Similarly to zip, a generator is used for the iterator here, precisely because it is undesirable to compute every value in advance. After all, these values may or may not be needed. Instead, each value is computed when and only when it is requested.
>>> m = map(lambda x, y: max([x, y]), [4, 1, 7], [3, 4, 5])
>>> next(m)
4
>>> next(m)
4
>>> next(m)
7
>>> next(m)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
As before, this is a trivial operation when dealing with small iterables. However, given a larger data structure, the use of a generator may entail serious savings in computation time or memory use, because the entire structure does not need to be computed and transformed at once.
One of the most commonly used generators in Python is the open file object. Although you can interact in many ways with open files in Python, and it is common with smaller files to just call read to read the entire file into memory, the file object does support the generator pattern, which reads the file from disk one line at a time. This is very important when operating on larger files. It is not always reasonable to read the entirety of a file into memory.
For historical reasons, file objects have a special method called readline used for reading a line at a time. However, the generator protocol is also implemented, and calling next on a file does the same thing.
Consider the following simple file:
$ cat lines.txt
line 1
line 2
line 3
line 4
line 5
You read it in the Python shell by using the built-in open function. The resulting object is, among other things, a generator.
>>> f = open('lines.txt')
>>> next(f)
'line 1\n'
>>> next(f)
'line 2\n'
Note that the generator reads one line at a time and yields the entire line, including the trailing newline (\n) character.
If you attempt to call next after the end of the file is reached, StopIteration is raised as expected.
>>> next(f)
'line 5\n'
>>> next(f)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
It is worth noting that __next__ and readline are not exact aliases for one another here. Once end of file is reached, __next__ raises StopIteration as it would for any other generator, whereas readline actually catches this exception and returns an empty string:
>>> next(f)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> f.readline()
''
Essentially, you have two primary reasons to write generators. Both of them spring from the same fundamental concept, which is determining the value only when it is needed, rather than well ahead of time.
The basic principle at play here is this: You do yourself no favors by having your code do a bunch of work or store a bunch of data in advance. Often, you may not need large chunks of data. Even if you need all of it, you are still doing unnecessary storage if you do not need all of it at once.
The two use cases that branch out from this fundamental principle are the need to access data in pieces, and the need to compute data in pieces.
The first (and probably most common) reason to write generators is to cover cases where you must access data in chunks, but where it is undesirable to store copies of the entire thing.
This is essentially what happens in the file object generator explored previously, as well as the dict.items (and family) methods. When dealing with small files, it is entirely reasonable to read the entire file into memory and do whatever work needs to be done against that in-memory string.
On the other hand, what if a file is large? What if you need to restructure a dictionary that is large? Sometimes, making a copy to manipulate data is not a feasible operation. This is where accessing data in pieces is a valuable capability.
When iterating over a large file with the generator method, it does not matter how large the file is. Each line will be read and yielded, one at a time. When iterating over a dictionary with dict.items, it does not matter whatsoever how large the source dictionary is. The iterator will iterate over it one piece at a time, and yield only that two-tuple.
The same principle applies to generators that you write. A generator is a useful tool in any situation where you want to iterate over a substantial amount of data, and it is unnecessary to store or copy the entirety of that data in memory at once.
The second common reason to write generators is to compute data only as it is needed. Consider the range function or the fibonacci function discussed earlier in this chapter. A program that must loop over each number between zero and a googleplex need not store a list of every number between those figures. It is sufficient to simply keep adding one until the maximum is reached.
Similarly, the fibonacci function does not need to compute every Fibonacci number (an impossible task, because there exists an infinite number of them—more on this shortly). It simply must determine the single next Fibonacci number and yield it back.
This can be important because sometimes the computation of each item in a sequence can be expensive. It is not useful to compute the entire series unnecessarily.
One aspect that the earlier discussion about the fibonacci function explored briefly is the fact that some sequences are actually infinite. In such cases, it is not possible to represent the entire sequence in a list, but a generator is capable of representing this.
This is because a generator is not concerned with being aware of every value it must generate. It only needs to generate the next one. It does not matter that the Fibonacci sequence goes on forever. As long as your generator stores the most recent two numbers in the sequence, it is perfectly reasonable to compute the next one.
There is nothing wrong with this. It is the responsibility of the code calling the generator in such cases to deal with the fact that the sequence that the generator represents is an infinite one, and to break out of the sequence when appropriate.
One important (and often overlooked) fact about generators is that many generators are singletons. This is most often the case when an object is both iterable and an iterator. Because the iterable simply returns self, calling iter on such an object repeatedly will return the same object. This essentially means that the object supports only one active iterator.
A simple generator function is not a singleton. Calling the function multiple times returns distinct generators, as shown here:
>>> gen1 = fibonacci()
>>> next(gen1), next(gen1), next(gen1), next(gen1), next(gen1)
(1, 1, 2, 3, 5)
>>> gen2 = fibonacci()
>>> next(gen2)
1
>>> next(gen1)
8
The following iterable class serves a similar purpose, and returns itself in its __iter__ method:
class Fibonacci(object):
def __init__(self):
self.numbers = []
def __iter__(self):
return self
def __next__(self):
if len(self.numbers) < 2:
self.numbers.append(1)
else:
self.numbers.append(sum(self.numbers))
self.numbers.pop(0)
return self.numbers[-1]
def send(self, value):
pass
# For Python 2 compatibility
next = __next__
This is a Fibonacci class, which implements the generator protocol. However, note that it is also iterable, and responds to iter… with itself. This means that each Fibonacci object has only one iterator: itself.
>>> f = Fibonacci()
>>> i1 = iter(f)
>>> next(i1), next(i1), next(i1), next(i1), next(i1)
(1, 1, 2, 3, 5)
>>> i2 = iter(f)
>>> next(i2)
8
There is nothing inherently wrong with this. It is worth noting, however, because some generators may be implemented as singletons, whereas others are not. Be aware of what the relationship is between the iterable and the iterators, and whether or not an iterable allows multiple iterators. Some do; others do not.
It is often desirable for functions to call other functions. This is a key way that developers structure code for reusability. Similarly, it is often desirable for generators to call other generators. Python 3.3 introduces the new yield from statement to provide a straightforward way for a generator to call out to other generators.
Consider the following two trivial, finite generators:
def gen1():
yield 'foo'
yield 'bar'
def gen2():
yield 'spam'
yield 'eggs'
Prior to Python 3.3, the common way to combine these subgenerators into one would be to iterate over them explicitly in the wrapping generator, as shown here:
def full_gen():
for word in gen1():
yield word
for word in gen2():
yield word
It is also possible to do this with the itertools.chain method:
def full_gen():
for word in itertools.chain(gen1(), gen2()):
yield word
The Python 3.3 yield from syntax provides a cleaner way to do the same thing, and looks much more in line with a function call within another function.
def full_gen():
yield from gen1()
yield from gen2()
Use of this syntax is referred to as generator delegation. And, in fact, the previous two implementations of full_gen are not actually equivalent. This is because the former implementation discards any value sent to the generator using send.
The yield from syntax, on the other hand, preserves this, because the generator is simply delegating to another generator. This means that any values sent to the wrapping generator will also be sent to the current delegate generator, avoiding the need for the developer to handle this.
Generators are valuable tools in Python that are used to perform computations or iterate over large amounts of data while only storing and computing what you actually need at the time. This can mean substantial cost savings in terms of both memory and performance.
Consider using generators when dealing with substantial amounts of data or computational work, when not all the work needs to be done in advance. Also consider generators as a way to represent infinite or branching sequences.
In Chapter 4, “Magic Methods,” you begin your study of classes in Python, starting with an introduction to magic methods.