Generator functions: send(), throw() and close()

We introduced generator functions as a way of writing functions that yield successive values while maintaining the function’s state in memory. We’ve seen how to use the yield statement to generate these values.

A generator function written this way does not allow any changes to the function’s variables to be passed in from the calling routine. In fact, there is a way of modifying a generator function’s behaviour from outside, by the use of the send() method. Its use is a bit subtle, however, so we need a few examples to see how it works.

First, we write a generator function that generates a list of random integers, where the function’s arguments are the length of the list and the range of numbers from which the random integers are to be selected.

from random import *

def genRandomList(lenList, rangeList):
    for i in range(10):
        ranList = [randint(2, 2 + rangeList) for i in range(lenList)]
        print('In genRandomList: ', ranList)
        vals = yield ranList
        if vals is not None:
            lenList = vals[0]
            rangeList = vals[1]

The function generates 10 random lists using list comprehension to generate each list on line 5.

There is a yield statement on line 7, but here the yield is given as the right operand of an assignment operator. We’ll see in a minute that when a generator yields a value to the caller, the caller has an opportunity to pass in some data to the generator. In this case, a two-element list is passed in and is assigned to vals. After a check that vals contains data on line 8, we assign its element [0] to be the new length of the list and its element [1] to be the new range of integers from which the random numbers are to be selected. Thus we have a way of changing the generator’s arguments after it has been called.

The send() method

The fact that we are assigning a yield to a variable means that the generator expects something to be sent to it whenever a yield is encountered. The way this is done is via the generator’s send() method. To illustrate how it works, consider this code:

genList = genRandomList(4, 10)
myList = next(genList)
print(myList)
myList = genList.send(myList[:2])
print(myList)
myList = next(genList)
print(myList)
myList = next(genList)
print(myList)

After initializing the generator, we call next() to generate the first list. Then we call send() with its argument being a list consisting of the first two elements of the myList that was generated in the first step. This is the value that is passed into the generator’s code as the value of the yield statement, and is what gets assigned to the vals variable on the left of the assignment operator. Since vals is not None, vals[0] is assigned to be the new list length and vals[1] is the new range of integers. The call to send() is followed with two more calls using next(). Typical output is:

In genRandomList:  [7, 9, 7, 8]
[7, 9, 7, 8]
In genRandomList:  [10, 6, 9, 8, 8, 8, 11]
[10, 6, 9, 8, 8, 8, 11]
In genRandomList:  [10, 10, 8, 8, 6, 8, 8]
[10, 10, 8, 8, 6, 8, 8]
In genRandomList:  [10, 2, 10, 9, 10, 10, 2]
[10, 2, 10, 9, 10, 10, 2]

The first list’s first two elements are 7 and 9, so the send() call results in the next list being of length 7 with integers in the range 2 to 11. Note that the call to send() acts like a call to next() but with the extra feature of assigning a value to the yield statement. That is, send() first assigns a value to vals and then runs the generator code until the next yield is encountered. In our example, this amounts to redefining the list’s parameters and then generating a new list.

The subsequent two calls to next() don’t affect the list’s parameters, so they are both generated with a length of 7 and random numbers between 2 and 11. In fact, a call to next() does pass something into the generator, but it’s the Python reserved word None. This is the reason we included the check if vals is not None on line 8 of the generator code above.

Now consider this code, in which we use a for loop to iterate through the generator:

genList = genRandomList(4, 10) 
for randList in genList:
    myList = next(genList)
    print(myList)

If you run this, you’ll find that only 5 lists are printed, rather than 10. The reason is that the for statement is implicitly calling next() on the generator object genList at the start of each iteration of the loop. The value from this call to next() is never used in the loop; rather we write an explicit call to next() and print out the value from that. If we deleted this explicit call and just printed out the loop variable randList, we would get all 10 lists. However, since we haven’t called send() anywhere, the parameters of the list are never changed, since next() always passes in None to be assigned to vals.

Exercise

Write some code that uses the genRandomList() generator above to generate n lists, where n is the number of lists generated by genRandomList(), and where each list uses the first two elements of the preceding list to define lenList and rangeList. In the above code, n is 10, but you should not have to know the value of n to write this code.

See answer

The key is to use the StopIteration exception to end the program cleanly. Suitable code is

genList = genRandomList(4, 10)
myList = next(genList)
while True:
    try:
        print(myList)
        myList = genList.send(myList[:2])
    except StopIteration:
        break

The final call to send() will generate the StopIteration.

Typical output (here with n = 13 in genRandomList()) is:

[2, 11, 6, 5]
[9, 11]
[7, 4, 9, 5, 3, 2, 6, 7, 2]
[2, 6, 5, 5, 3, 3, 4]
[8, 5]
[5, 2, 7, 2, 5, 3, 7, 2]
[4, 4, 2, 4, 4]
[3, 2, 6, 2]
[2, 4, 2]
[5, 5]
[2, 6, 5, 6, 7]
[7, 7]
[3, 6, 2, 3, 2, 8, 4]

The throw() method

The above example relied on the generator running out of yields and thus raising a StopIteration exception. We can force a generator to throw an exception (of any type, and at any time) by using the throw() method. A simple example is the following, still using our genRandomList() generator.

genList = genRandomList(4, 10)
myList = next(genList)
while True:
    print(myList)
    if len(myList) > 5:
        genList.throw(ValueError('List is too long'))
    myList = genList.send(myList[:2])

Running this code gives the typical output:

[2, 7, 3, 7]
[8, 8]
[8, 6, 2, 2, 5, 5, 2, 10]
Traceback (most recent call last):
  File "D:\Documents\Programming\programmingpages\Python\Random list 01\Random list 01\Random_list_01.py", line 21, in <module>
    genList.throw(ValueError('List is too long'))
  File "D:\Documents\Programming\programmingpages\Python\Random list 01\Random list 01\Random_list_01.py", line 6, in genRandomList
    vals = yield ranList
ValueError: List is too long

As soon as the list length exceeds 5, a ValueError is thrown, with the message that we passed to it. Typically you would use a try block to catch these exceptions and provide some clean way of handling them.

The close() method

The close() method is really just a specialized version of throw(), in that it always throws a StopIteration. We could replace the line genList.throw(ValueError('List is too long')) with genList.close() in the above example, and a StopIteration would be thrown when the first list with a length greater than 5 was generated.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.