Python / Coroutines

Coroutines

By Marcelo Fernandes Aug 28, 2017


Shortcuts

Basic Coroutine Example
The 4 Coroutine States
Multiple Yield example
Decorators for coroutine priming
Coroutine Termination and Exception Handling
Returning a value from a coroutine
Coroutine States
Using yield from
Rules of Thumb for Yield From

Meaning of Yield

There are two main meanings for the ver "to yield". The first one is to "produce", the second one is "to give way".

Both of them justify the usage of this verb for python.

If you know a little bit about python generators, you may realize that syntactically they are very alike. However, in a coroutine the yield statement usually appears on the right side of an expression, example: x = yield, and it may or may not "produce" or "give way" to a value.

if there is no expression after the "yield" it returns None, otherwise it returns the expression. Because of this particular behaviour, yield is a control flow device that might be used to implement cooperative multi-tasking. We might build up an environment in which each coroutine yields control to other subcoroutines or functions that are centralized by a scheduler.


Basic Coroutine Example


def coroutine():
    print("---Started---")
    x = yield
    print("received: ", x)

foo = coroutine()
foo
# <generator object coroutine at 0x7f42f280f308>

next(foo)
# ---Started---

foo.send(1994)
# received: 1994

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration



The first call in a coroutine is next(...). Once the generator hasn't started yet, it is not waiting in a yield statement, and we can't send data to it when it is not holding this state. (we can send data only on yield states)

As usual, when the control flows off the end of the coroutine body, it raise a StopIteration. This is exception is treated by the for loops, so they have an idea that the iterator came to an end.

Coroutine States

A coroutine can be in one of four states, and this state can be determined using inspect.getgeneratorstate(...), which will return either:

  • 'GEN_CREATED': Waiting to start execution
  • 'GEN_RUNNING': Currently being executed by the interpreter (this is hard to see in practice, but more common in multi-threaded applications)
  • 'GEN_SUSPENDED': Currently suspended at a yield expression.
  • 'GEN_CLOSED': Execution has finished.

The initial call next(coroutine) is often described as “priming” the coroutine, i.e. advancing it to the first yield to make it ready for use as a live coroutine

Multiple Yield example:


from inspect import getgeneratorstate


def simple_coro(a):
    print('-> Started: a =', a)
    b = yield a
    print('-> Received b=', b)
    c = yield a + b
    print('-> Received c=', c)

coroutine = simple_coro(20)
getgeneratorstate(coroutine)
# 'GEN_CREATED'

next(coroutine)
# -> Started: a=20
# 20

getgeneratorstate(coroutine)
# 'GEN_SUSPENDED'

coroutine.send(30)
# -> Received b=30
# 50

coroutine.send(40)
# -> Received c=40

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

getgeneratorstate(coroutine)
# 'GEN_CLOSED'

It’s crucial to understand that the execution of the coroutine is suspended exactly at the yield keyword. As mentioned before, in an assignment statement the code to the right of the = is evaluated before the actual assignment happens. This means that in a line like b = yield a the value of b will only be set when the coroutine is activated later by the client code.

This is what it means:




Decorators for coroutine priming

It is not so good be calling next(coroutine) every time before calling coroutine.send(x), some libraries already implement their coroutines using a wrapper in order to get rid of this behaviour. In our case, a simple decorator could be used to do so:


from functools import wraps

def coroutine(func):
    """Primes 'func' by advancing to first 'yield'"""
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen

    return primer

This is pretty straight forward. But you may ask: "Why are we using functools.wraps?"

Remember that when we use a decorator, our function that is being decorated loses its data. It will lose its name, docstring, arguments list, etc. Everything will be replaced by the decorator function arguments, even our IDE will go crazy, instead of seeing the generator function arguments when we hover the mouse over the method that we are calling, we are gonna see (*args, **kwargs), which are the arguments that the primer function takes. You can check more references about this subject here!

Important: When we use the syntax yield from it automatically primes the coroutine called by it, making it incompatible with our decorator.




Coroutine Termination and Exception Handling

Coroutines have two methods that allow the client to explicitly send exceptions into the coroutine

  • generator.throw(): Causes the yield expression where the generator paused to raise a given exception. If the exception is treated by the coroutine, flows advances to the next yield. If not, it is propagated back to the caller
  • generator.close(): Makes the yield expression to raise a Generator Exit exception. No error is reported to the caller if the generator does not handle this exception or raises StopIteration. Besides that, when receiving a GeneratorExit, the generator must not yield a value, otherwise a RunTimeError is raised.

class CustomException(Exception):
    pass

def coro_exception():
    print('-- started')
    while True:
        try:
            x = yield
        except CustomException:
            print('-- CustomException Handled')
        else:
            print('-- received: ', x)

exec_coro = coro_exception()
next(exec_coro)
# -- started
exec_coro.send('x')
# -- received: x
exec_coro.throw(CustomException)
# -- CustomException Handled
exec_coro.close()
exec_coro.send(10)
Traceback (most recent call last):
StopIteration

from inspect import getgeneratorstate
getgeneratorstate(exec_coro)
# 'GEN_CLOSED'




Returning a value from a coroutine

Some coroutines do not yield anything interesting, but are designated to return a value when finishing, often a result of some accumulation.

In order to return a value, a coroutine must terminate normally. It might happen whenever the accumulation loop is finished. Check the next example.



def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        count += 1
        average = total/count

    return average

avg = averager()
next(avg)
avg.send(10)
avg.send(5)
avg.send(3)
avg.send(None)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration: 6.0

Look how the result of our averager function is returned stuck inside the "StopIteration" exception value.

Again, the yield from treats this problem by catching StopIteration internally, but as we are not using this syntax we have to do something like:


try:
    avg.send(None)
except StopIteration as exc:
    result = exc.value

result
# 6.0

This "hacky" solution is ok though, it is stated in PEP 380. When we are using yield from, it not only consumes the StopIteration, but its value becomes the value of the yield from expression itself. Pretty cool?




Using yield from

Similar constructs in other languages are called await and that is a much better name because it conveys a crucial point: when a generator gen calls yield from subgen(), the subgen takes over and will yield values to the caller of gen; the caller will in effect drive subgen directly. Meanwhile gen will be blocked, waiting until subgen terminantes.

The first thing the yield from x expression does with the x object is to call iter(x) to obtain an iterator from it. This means that x can be any iterable

However, if replacing nested for loops yielding values was the only contribution of yield from, this language addition wouldn’t have had a good chance of being accepted. The real nature of yield from cannot be demonstrated with simple iterables, it requires the mind-expanding use of nested generators. That’s why PEP 380 which introduced yield from is titled “Syntax for Delegating to a Subgenerator”.

The main feature of yield from is to open a bidirectional channel from the outermost caller to the innermost subgenerator, so that values can be sent and yielded back and forth directly from them, and exceptions can be thrown all the way in without adding a lot of exception handling boilerplate code in the intermediate coroutines. This is what enables coroutine delegation in a way that was not possible before.

Good examples can be found in what is new in python 3.3


# BEGIN YIELD_FROM_AVERAGER
from collections import namedtuple

Result = namedtuple('Result', 'count average')


# the subgenerator
def averager():  # <1>
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield  # <2>
        if term is None:  # <3>
            break
        total += term
        count += 1
        average = total/count
    return Result(count, average)  # <4>


# the delegating generator
def grouper(results, key):  # <5>
    while True:  # <6>
        results[key] = yield from averager()  # <7>


# the client code, a.k.a. the caller
def main(data):  # <8>
    results = {}
    for key, values in data.items():
        group = grouper(results, key)  # <9>
        next(group)  # <10>
        for value in values:
            group.send(value)  # <11>
        group.send(None)  # important! <12>
    report(results)


# output report
def report(results):
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print('{:2} {:5} averaging {:.2f}{}'.format(
              result.count, group, result.average, unit))


data = {
    'girls;kg':
        [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
    'girls;m':
        [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
    'boys;kg':
        [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
    'boys;m':
        [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}

main(data)

# 9 boys  averaging 40.42kg
# 9 boys  averaging 1.39m
# 10 girls averaging 42.04kg
# 10 girls averaging 1.43m


A good explanation for what happens during the "yield from" call is in part of the PEP 380:

“When the iterator 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 expression. Furthermore, the subgenerator is allowed to execute a return statement with a value, and that value becomes the value of the yield from expression.”




Rules of Thumb for Yield From

Some insights about coroutines and yield from:

  • Any values that the subgenerator yields are passed directly to the caller of the delegating generator i.e. the client code.
  • Any values sent to the delegating generator using send() are passed directly to the subgenerator. If the sent value is None, the subgenerator’s __next__() method is called. If the sent value is not None, the subgenerator’s send() method is called. If the call raises StopIteration, the delegating generator is resumed. Any other exception is propagated to the delegating generator.
  • return expr in a generator (or subgenerator) causes StopIteration(expr) to be raised upon exit from the generator.
  • The value of the yield from expression is the first argument to the StopIteration exception raised by the subgenerator when it terminates.
  • Exceptions other than GeneratorExit thrown into the delegating generator are passed to the throw() method of the subgenerator. If the call raises StopItera tion, 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 subgenerator 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.