Introduction to Coroutines
Last week, I wrote about the
yield
keyword in Python. To continue on with a series of posts about my new
favorite topic, I'll introduce the basics of coroutines by building on the
yield
keyword.
Every programmer is familiar with functions. A basic coroutine is defined just like a function in
Python. Like generator functions, it delegates to the yield
keyword to do its
magic. However, unlike generators, the yield
keyword is placed on the right side
of the assignment operator, and it actually returns a value that can be passed from the calling
code.
It might be best to show an example and then to explain the features:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Define the coroutine
def accumulate():
# Code up to the 'yield' will be executed
# upon 'priming' the coroutine
total = 0
while True:
# At this point, the context will switch
# to the caller. The caller will receive
# the value currently in 'total'. The
# caller can then pass 'new_num' to this
# coroutine to be added to 'total'
new_num = yield total
total += new_num
# Create the coroutine. Note that the coroutine
# must be called - this actually tripped me up
# the first time (I thought it had to be a handle)
accumulator = accumulate()
# Call 'next' to 'prime' the coroutine
next(accumulator)
# Print the cumulative sum of 1 through 10
for ii in range(10):
print(accumulator.send(ii), end=' ')
This coroutine will then print:
0 1 3 6 10 15 21 28 36 45
As you can see, the accumulate()
coroutine keeps a cumulative sum of each number
that is passed to it by the calling function. Each time a new number is passed to it, it returns
the current sum. The main method body simply prints the output from accumulating 0 through 9.
Let's start with the syntax of the coroutine body. It looks just like a function, except it does
not have a return
statement. Coroutines can have one, but at that point it stops
iterating forever. We'll cover that below. Instead of a return
, it has a
yield
statement to pass values back to the calling code. Otherwise, coroutine
bodies are just like method bodies. The trickiest part is that when the yield
keyword is encountered, the context is switched back to the calling code. The state of the
coroutine is frozen until the calling code calls the coroutine again with send()
or next()
.
Note that coroutines don't need to pass anything back to the calling code. Instead, they can be delegated to for their side effects only. For example, an accumulator which simply prints the sum (instead of returning it) would look like this:
1
2
3
4
5
def accumulate():
total = 0
while True:
total += yield
print(total)
This is completely valid, though the syntax looks a little strange.
Before continuing on, it's important to understand the difference between calling
next()
and send()
. The next()
function is
almost like an alias for send(None)
. There are two cases to familiarize yourself
with here.
The first case is when 'priming' the coroutine. This mechanism is needed to tell the coroutine to
execute to the first yield keyword. The only acceptable value that the calling code can pass in
this case is None
. Trying to pass something other than None
to
an un-primed coroutine creates an error:
>>> def accumulate():
... total = 0
... while True:
... total += yield
... print(total)
...
>>> accumulator = accumulate()
>>> accumulator.send(10)
Traceback (most recent call last):
File '', line 1, in
TypeError: can't send non-None value to a just-started generator
Make sure to prime those coroutines! A really cool design pattern that is covered by Luciano Ramalho's Fluent Python is to define a priming decorator. That pattern is out of scope for this blog post, but it shouldn't be hard to find online (or just buy his book - he's coming out with a new version soon!).
The second case to note is when next()
or send(None)
is called
on a coroutine which is expecting a value other than None
. A coroutine will need
to handle this case or a runtime error will result:
>>> import time
>>> def state_machine():
... states = ['READY', 'SET', 'GO!']
... for state in states:
... timeout = yield(state)
... time.sleep(timeout)
...
>>> state_machine_co = state_machine()
>>> next(state_machine_co)
'READY'
>>> while True:
... print(state_machine_co.send(2))
...
SET
GO!
Traceback (most recent call last):
File '', line 2, in
StopIteration
The above state machine waits for the calling code to pass a timeout value. Before transitioning
to the next state, it waits for the timeout and then prints out the next statement to the user.
It also shows an interesting point about the flow control of the yield
keyword.
The first call to next()
actually returns the first state in the list, instead
of waiting until the calling code delegates back to the coroutine. This makes sense, since Python
executes the entire right side of a statement before assigning to the left side of the assignment
operator (in this case 'timeout').
Try this example yourself!
The last topic to cover in this second installment of coroutines is returning a value. Let's go
back to our accumulator method. Instead of returning a value with each iteration, suppose that
you wanted to return a final value after all numbers have been accumulated. This functionality
can be supported with coroutines. We just need to send None
from the caller to
signal that the list has been exhausted. Here's how it would be implemented:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def accumulate():
total = 0
while True:
new_num = yield
if new_num is None:
break
total += new_num
return total
accumulator = accumulate()
accum_inputs = list(range(10))
accum_inputs.insert(0, None)
accum_inputs.append(None)
try:
for ii in accum_inputs:
accumulator.send(ii)
except StopIteration as exception:
print(f'Sum is {exception.value}')
This code prints the following:
Sum is 45
Let's break this down a little bit, starting with the yield
keyword. Since it's
alone on the right side of the assignment operator, it returns None
to the
calling code, so the return value of send()
can be ignored. The
new_num
attribute is then checked for None
. If it is
None
, the while
loop is exited and the total is returned to the
calling code. At this point, the StopIteration
exception is raised.
As you can see, the return value is actually encapsulated in the exception (if you think that's
a little bit ugly, I'd have to agree). To access the return value is through the value attribute
of the StopIteration
exception. Any object can be encapsulated in the exception,
including tuples which can be used to return multiple values.
After reading about coroutines, I was pretty disappointed that this was the best that Python could do to return a value. However, I was proved wrong. Python has a much better way to return values from coroutines. It will be the subject of next week's post.