circuitpython_rawpixel-648581-unsplash.jpg
Photo by rawpixel on Unsplash

Generators are a convenient way to make custom iterators. They take two forms: generator functions and generator expressions.

Generator Functions

These are functions that result in a generator object (basically an iterator) rather than a function object. That object can then be used in a for loop or passed to the next function. The differentiating feature of a generator function vs. a regular function is the use of the yield statement.

For example, if we wanted something like range that instead worked on floats, we could write one as a generator function:

def range_f(start, stop, step):
    x = start
    while x <= stop:
        yield x
        x += step

Now we can write code such as:

for x in range_f(0, 1.0, 0.125):
    print(x)

That gives output of:

0
0.125
0.25
0.375
0.5
0.625
0.75
0.875
1.0

And we can use it with next:

r = range_f(0, 1, 0.125)
next(r)
#0
next(r)
#0.125
next(r)
#0.25
next(r)
#0.375
next(r)
#0.5

The first time the resulting generator object is used, the function starts executing from the beginning. In this case, it creates a local variable x and sets it to the value of start.  The while loop executes, and the yield statement is encountered. This pauses execution of the function and produces its argument (the current value ofxin this case). The next time the generator object is used (iterating in a for loop or passed to a call to next), the function resumes execution immediately after the yield statement. In this case, x is advanced, and the while loop goes back to the condition check. This continues until the iteration is complete (x > stop in this example). When that happens, the function continues past the loop, possibly doing some wrap up before exiting. When it exits a StopIteration exception is raised.

next(r)
#1.0
next(r)
#Traceback (most recent call last):
#File "", line 1, in 
#StopIteration:

Generator Expressions

Generator expressions use the same syntax as list comprehensions, but enclose it in parentheses rather than brackets:

(x for x in range(10))
#<generator object '<genexpr>' at 20002590>

Notice that the result isn't a list. It's a generator object as we saw above. To get values from it, we call next passing in the generator object:

g = (x for x in range(10))
next(g)
#0
next(g)
#1
next(g)
#2
next(g)
#3
next(g)
#4

We can also take g and use it in a for loop:

g = (x for x in range(10))
for x in g:
    print(x)
#0
#1
#2
#3
#4
#5
#6
#7
#8
#9

Now, think about how you would go about processing items from an infinitely long list. "But," you say, "where would be have an infinite amount of data in a microcontroller context?"  How about when you're taking temperature readings, and you continue taking temperature readings for the entire time the system is running. That is effectively an infinitely long list of data. And infinite really just means that: you have no idea how long it is.

While you could just read the temperature in the main while True loop (and that's perfectly fine for a simple application that is mainly just a while True loop) as your code grows and you want to organize it in a cleaner and more modular way (we are assuming that you're running on at least a SAMD51 chip, if not a Raspberry Pi, so you have plenty of room for large and well structured code). It can be cleaner to encapsulate that sequence of readings in an iterator. This lets it be passed around as needed and asked for another value where and when desired.

Another nice thing about using iterators is that they are now objects and can be worked with. There are ways to combine and ask for values from them in clean, readable ways.

Examples

One neat thing we can do with generators is to make infinite sequences. These don't have a condition to end the iteration. For example, we can make an infinite sequence of integers:

def ints():
    x = 0
    while True:
        yield x
        x += 1

We can use this with next to get a ongoing sequence of integers, but that's not very interesting.

>>> i = ints()
>>> next(i)
0
>>> next(i)
1
>>> next(i)
2
>>> next(i)
3
>>> next(i)
4

Far more interesting is to use ints() in other generators. Here are some simple examples:

evens = (x for x in ints() if x % 2 == 0)
odds = (x for x in ints() if x % 2 != 0)
squares = (x**2 for x in ints())

Note that each use of ints() returns a new and unique generator object.

This guide was first published on Oct 11, 2018. It was last updated on Sep 16, 2018.

This page (Generators) was last updated on Sep 17, 2018.

Text editor powered by tinymce.