Python Tutorial_ Generators
Python Tutorial_ Generators
Home Python 2 Tutorial Python 3 Tutorial Advanced Topics Numerical Programming Machine Learning Tkinter Tutorial Contact
Generators
Method of working:
A generator is called like a function. Its return value is an iterator, i.e. a generator object. The code of the generator will not be executed at this stage.
The iterator can be used by calling the next method. The first time the execution starts like a function, i.e. the first line of code within the body of the iterator. The code is executed until a yield statement is Data Protection
reached. Declaration
yield returns the value of the expression, which is following the keyword yield. This is like a function, but Python keeps track of the position of this yield and the state of the local variables is stored for the next
call. At the next call, the execution continues with the statement following the yield statement and the variables have the same values as they had in the previous call. Data Protection
The iterator is finished, if the generator body is completely worked through or if the program flow encounters a return statement without a value. Declaration
We will illustrate this behaviour in the following example, in which we define a generator which generates an iterator for all the Fibonacci numbers.
The Fibonacci sequence is named after Leonardo of Pisa, who was known as Fibonacci (a contraction of filius Bonacci, "son of Bonaccio"). In his textbook Liber Abaci, which appeared in the year 1202) he had an
exercise about the rabbits and their breeding: It starts with a newly-born pair of rabbits, i.e. a male and a female animal. It takes one month until they can mate. At the end of the second month the female gives
birth to a new pair of rabbits. Now let's suppose that every female rabbit will bring forth another pair of rabbits every month after the end of the first month. We have to mention that Fibonacci's rabbits never die.
They question is how large the population will be after a certain period of time.
Fn = Fn - 1 + Fn - 2
with the seed values:
F0 = 0 and F1 = 1
def fibonacci(n):
""" A generator for creating the Fibonacci numbers """
a, b, counter = 0, 1, 0
while True:
if (counter > n):
return
yield a
a, b = b, a + b
counter += 1
f = fibonacci(5)
for x in f:
print(x, " ", end="") #
print()
The generator above can be used to create the first n Fibonacci numbers separated by blanks, or better (n+1) numbers because the 0th number is also included.
In the next example we present a version which is capable of returning an endless iterator. We have to take care when we use this iterator that a termination criterion is used:
def fibonacci():
"""Generates an infinite sequence of Fibonacci numbers on demand"""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
f = fibonacci()
counter = 0
for x in f:
print(x, " ", end="")
counter += 1
if (counter > 10):
break
print()
Since Python 3.3, generators can also use return statements, but a generator still needs at least one yield statement to be a generator! A return statement inside of a generator is equivalent to raise StopIteration()
We demonstrate now that return is equivalent, or "nearly", if we disregard one line of the traceback:
Generators can not only send objects but also receive objects. Sending a message, i.e. an object, into the generator can be achieved by applying the send method to the generator object. Be aware of the fact that
send both sends a value to the generator and returns the value yielded by the generator. We will demonstrate this behavior in the following simple example of a coroutine:
>>> next(cr)
coroutine has been started!
>>> cr.send("Hi")
coroutine received: Hi
Traceback (most recent call last):
File "", line 1, in
StopIteration
>>>
We had to call next on the generator first, because the generator needed to be started. Using send to a generator which hasn't been started leads to an exception:
>>> cr = simple_coroutine()
>>> cr.send("Hi")
Traceback (most recent call last):
File "", line 1, in
TypeError: can't send non-None value to a just-started generator
>>>
To use the send method the generator has to wait at a yield statement, so that the data sent can be processed or assigned to the variable on the left side. What we haven't said so far: A next call also sends and
receives. It always sends a None object. The values sent by "next" and "send" are assigned to a variable within the generator: this variable is called "message" in the following example. We called the generator
infinit_looper, because it takes a sequential data objects and creates an iterator, which is capable of looping forever over the object, i.e. it starts again with the first element after having delivered the last object. By
sending an index to the iterator, we can continue at an arbitrary position.
def infinite_looper(objects):
count = 0
while True:
if count >= len(objects):
count = 0
message = yield objects[count]
if message != None:
count = 0 if message < 0 else message
else:
count += 1
We demonstrate how to use this generator in the following interactive session, assuming that the generator is saved in a file called generator_decorator.py:
The throw() method raises an exception at the point where the generator was paused, and returns the next value yielded by the generator. It raises StopIteration if the generator exits without yielding another
value. The generator has to catch the passed-in exception, otherwise the exception will be propagated to the caller. The infinite_looper from our previous example keeps yielding the elements of the sequential data,
but we don't have any information about the index or the state of the variable "count". We can get this information by throwing an exception with the "throw" method. We catch this exception inside of the generator
and print the value of "count":
def infinite_looper(objects):
count = 0
while True:
if count >= len(objects):
count = 0
try:
message = yield objects[count]
except Exception:
print("index: " + str(count))
if message != None:
count = 0 if message < 0 else message
else:
count += 1
We can improve the previous example by defining our own exception class StateOfGenerator:
class StateOfGenerator(Exception):
def __init__(self, message=None):
self.message = message
def infinite_looper(objects):
count = 0
while True:
if count >= len(objects):
count = 0
try:
message = yield objects[count]
except StateOfGenerator:
print("index: " + str(count))
if message != None:
count = 0 if message < 0 else message
else:
count += 1
Decorating Generators
There is one problem with our approach, we cannot start the iterator by sending directly an index to it. Before we can do this, we need to use the next function to start the iterator and advance it to the yield
statement. We will write a decorator now, which can be used to make a decorator ready, by advancing it automatically at creation time to the yield statement. This way, it will be possible to use the send method
directly after initialisation of a generator object.
def get_ready(gen):
"""
Decorator: gets a generator gen ready
by advancing to first yield statement
"""
@wraps(gen)
def generator(*args,**kwargs):
g = gen(*args,**kwargs)
next(g)
return g
return generator
@get_ready
def infinite_looper(objects):
count = -1
message = yield None
while True:
count += 1
if message != None:
count = 0 if message < 0 else message
if count >= len(objects):
count = 0
message = yield objects[count]
x = infinite_looper("abcdef")
print(next(x))
print(x.send(4))
print(next(x))
print(next(x))
print(x.send(5))
print(next(x))
a
e
f
a
f
a
You might have noticed that we have changed the generator infinite_looper a little bit as well.
yield from
We can learn from the following example by looking at the two generators 'gen1' and 'gen2' that yield from is substituting the for loops of 'gen1':
def gen1():
for char in "Python":
yield char
for i in range(5):
yield i
def gen2():
yield from "Python"
yield from range(5)
g1 = gen1()
g2 = gen2()
print("g1: ", end=", ")
for x in g1:
print(x, end=", ")
print("\ng2: ", end=", ")
for x in g2:
print(x, end=", ")
print()
We can see from the output that both generators are the same:
g1: , P, y, t, h, o, n, 0, 1, 2, 3, 4,
g2: , P, y, t, h, o, n, 0, 1, 2, 3, 4,
The benefit of a yield from statement can be seen as a way to split a generator into multiple generators. That's what we have done in our previous example and we will demonstrate this more explicitely in the
following example:
def cities():
for city in ["Berlin", "Hamburg", "Munich", "Freiburg"]:
yield city
def squares():
for number in range(10):
yield number ** 2
def generator_all_in_one():
for city in cities():
yield city
for number in squares():
yield number
def generator_splitted():
yield from cities()
yield from squares()
The previous code returns True because the generators generator_all_in_one and generator_splitted yield the same elements. This means that if the <expr> from the yield from is another generator, the effect is the same as if the
body of the sub‐generator were inlined at the point of the yield from statement. Furthermore, the subgenerator is allowed to execute a return statement with a value, and that value becomes the value of the yield
from expression. We demonstrate this with the following little script:
def subgenerator():
yield 1
return 42
def delegating_generator():
x = yield from subgenerator()
print(x)
for x in delegating_generator():
print(x)
1
42
The full semantics of the yield from expression is described in six points in "PEP 380 -- Syntax for Delegating to a Subgenerator" in terms of the generator protocol:
Any values that the iterator yields are passed directly to the caller.
Any values sent to the delegating generator using send() are passed directly to the iterator. If the sent value is None, the iterator's __next__() method is called. If the sent value is not None, the iterator's send()
method is called. If the call raises StopIteration, the delegating generator is resumed. Any other exception is propagated to the delegating generator.
Exceptions other than GeneratorExit thrown into the delegating generator are passed to the throw() method of the iterator. If the call raises StopIteration, the delegating generator is resumed. Any other exception is
propagated to the delegating generator.
If a GeneratorExit exception is thrown into the delegating generator, or the close() method of the delegating generator is called, then the close() method of the iterator is called if it has one. If this call results in an
exception, it is propagated to the delegating generator. Otherwise, GeneratorExit is raised in the delegating generator.
The value of the yield from expression is the first argument to the StopIteration exception raised by the iterator when it terminates.
return expr in a generator causes StopIteration(expr) to be raised upon exit from the generator.
Recursive Generators
Like functions generators can be recursively programmed. The following example is a generator to create all the permutations of a given list of items.
For those who don't know what permutations are, we have a short introduction:
Formal Definition:
A permutation is a rearrangement of the elements of an ordered list. In other words: Every arrangement of n elements is called a permutation.
In the following lines we show you all the permutations of the letter a, b and c:
abc
acb
bac
bca
cab
cba
The permutation generator can be called with an arbitrary list of objects. The iterator returned by this generator generates all the possible permutations:
def permutations(items):
n = len(items)
if n==0: yield []
else:
for i in range(len(items)):
for cc in permutations(items[:i]+items[i+1:]):
yield [items[i]]+cc
The previous example can be hard to understand for newbies. As often, Python offers a convenient solution. We need the module itertools for this purpose. Itertools is a very handy tool to create and operate on
iterators.
The term "permutations" can sometimes be used in a weaker meaning. Permutations can denote in this weaker meaning a sequence of elements, where each element occurs just once, but without the requirement
to contain all the elements of a given set. So in this sense (1,3,5,2) is a permutation of the set of digits {1,2,3,4,5,6}. We can build, for example, all the sequences of a fixed length k of elements taken from a given
set of size n with k ≤ n.
These atypical permutations are also known as sequences without repetition. By using this term we can avoid confusion with the term "permutation". The number of such k-permutations of n is denoted by Pn,k
and its value is calculated by the product:
n · (n - 1) · … (n - k + 1)
By using the factorial notation, the above expression can be written as:
Pn,k = n! / (n - k)!
A generator for the creation of k-permuations of n objects looks very similar to our previous permutations generator:
A Generator of Generators
The second generator of our Fibonacci sequence example generates an iterator, which can theoretically produce all the Fibonacci numbers, i.e. an infinite number. But you shouldn't try to produce all these numbers,
as we would do in the following example:
list(fibonacci())
This will show you very fast the limits of your computer.
In most practical applications, we only need the first n elements of an "endless" iterator. We can use another generator, in our example firstn, to create the first n elements of a generator g:
The following script returns the first 10 elements of the Fibonacci sequence:
#!/usr/bin/env python3
def fibonacci():
"""Ein Fibonacci-Zahlen-Generator"""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(firstn(fibonacci(), 10)))
Exercises
for time in trange((10, 10, 10), (13, 50, 15), (0, 15, 12) ):
print(time)
will return
3. Write a version "rtrange" of the previous generator, which can receive message to reset the start value.
4. Write a program, using the newly written generator "trange", to create a file "times_and_temperatures.txt". The lines of this file contain a time in the format hh::mm::ss and random temperatures between
10.0 and 25.0 degrees. The times should be ascending in steps of 90 seconds starting with 6:00:00.
For example:
06:00:00 20.1
06:01:30 16.1
06:03:00 16.9
06:04:30 13.4
06:06:00 23.7
06:07:30 23.6
06:09:00 17.5
06:10:30 11.0
5. Write a generator with the name "random_ones_and_zeroes", which returns a bitstream, i.e. a zero or a one in every iteration. The probability p for returning a 1 is defined in
a variable p. The generator will initialize this value to 0.5. This means that zeroes and ones will be returned with the same probability.
1. def running_average():
total = 0.0
counter = 0
average = None
while True:
term = yield average
total += term
counter += 1
average = total / counter
current = list(start)
while current < list(stop):
yield tuple(current)
seconds = step[2] + current[2]
min_borrow = 0
hours_borrow = 0
if seconds < 60:
current[2] = seconds
else:
current[2] = seconds - 60
min_borrow = 1
minutes = step[1] + current[1] + min_borrow
if minutes < 60:
current[1] = minutes
else:
current[1] = minutes - 60
hours_borrow = 1
hours = step[0] + current[0] + hours_borrow
if hours < 24:
current[0] = hours
else:
current[0] = hours -24
if __name__ == "__main__":
for time in trange((10, 10, 10), (13, 50, 15), (0, 15, 12) ):
print(time)
current = list(start)
while current < list(stop):
new_start = yield tuple(current)
if new_start != None:
current = list(new_start)
continue
seconds = step[2] + current[2]
min_borrow = 0
hours_borrow = 0
if seconds < 60:
current[2] = seconds
else:
current[2] = seconds - 60
min_borrow = 1
minutes = step[1] + current[1] + min_borrow
if minutes < 60:
current[1] = minutes
else:
current[1] = minutes - 60
hours_borrow = 1
hours = step[0] + current[0] + hours_borrow
if hours < 24:
current[0] = hours
else:
current[0] = hours -24
if __name__ == "__main__":
ts = rtrange((10, 10, 10), (13, 50, 15), (0, 15, 12) )
for _ in range(3):
print(next(ts))
print(ts.send((8, 5, 50)))
for _ in range(3):
print(next(ts))
fh = open("times_and_temperatures.txt", "w")
5. You can find further details and the mathematical background about this exercise in our chapter on Weighted Probabilities.
import random
def random_ones_and_zeros():
p = 0.5
while True:
x = random.random()
message = yield 1 if x < p else 0
if message != None:
p = message
x = random_ones_and_zeros()
next(x) # we are not interested in the return value
for p in [0.2, 0.8]:
print("\nWe change the probability to : " + str(p))
x.send(p)
for i in range(20):
print(next(x), end=" ")
print()
© 2011 - 2018, Bernd Klein, Bodenseo; Design by Denise Mitchinson adapted for python-course.eu by Bernd Klein