Python coroutine

Understanding coroutine in python by using summer problem

Read time: 7 min
Written on March 08, 2023

Easy problem and Typical solution

Imagine a problem where you need to keep track of sum of integer. Let us call it problem of Summer (also a season). Typical solution to the problem is to store sum in object. We will move from this solution towards one which demonstrate working of coroutine.

The typical solution will involve object which takes integer as input. It stores the sum as state and provides the output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Summer:
    def __init__(self, total=0):
        self.total = total
    def add(self, m=0):
        self.total += m
    def sum(self):
        return self.total

summer = Summer()
summer.add(10)
assert summer.sum() == 10

Let's call some objects

Instead of having two different methods, a single method can used. The said method can update the sum with input and return the sum as output. The default value of 0 as input to this will emulate sum from previous example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class SummerV0:
    def __init__(self, total=0):
        self.total = total
    def __call__(self, m=0):
        self.total += m
        return self.total

summer = SummerV0()
assert summer(10) == 10
assert summer() == 10

Object can be called as function by implementing __call__ function. This allow calling the object like any other function in python. Calling the object will call the __call__ method with the argument specified.

Summer using coroutine and globals

In previous example we used a single state which is updated when the object is called. This could also be achieved by using coroutine with similar interface.

Coroutine looks similar to function in python but the differ in certain ways. They can store state and yield the output instead of return-ing the output. In this regard they are closer to generator rather than function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def summer_v1(total_init=0):
    global total
    total = total_init
    while True:
        z = yield
        total += z if z else 0

summer = summer_v1()
next(summer)
summer.send(10)
assert total == 10

In the above example you see that we are using global variable to access the state. The coroutine returns a coroutine, this is initialized by calling next on the same. The initialization of coroutine will run the code till first yield. Once initialized the coroutine, input can be sent by using send interface. The value enters the coroutine at yield and can be used by the coroutine as it see fit.

No global please!!!

In previous example globals was used to access the state. This would mean having two coroutine would not be possible at same time. The problem can be avoided if coroutine could return the value of the state. The yield can be followed by value that can be returned by send interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def summer_v2(total=0):
    total = total
    while True:
        z = yield total
        total += z if z else 0

summer = summer_v2()
summer_again = summer_v2(12)
assert next(summer) == 0
assert next(summer_again) == 12
assert summer.send(10) == 10
assert summer_again.send(10) == 22

Automatic Initialization of coroutine

The initialization of coroutine from coroutine is needed before sending values. Sending value before initialization will raise exception. The message will be TypeError: can't send non-None value to a just-started coroutine. To avoid such accident we can decorate the coroutine with a safety_wrapper.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def safety_wrapper(cr):
    """the safety wrapper"""
    def init_cr(*args, **kwargs):
        dec_cr = cr(*args, **kwargs)
        dec_cr.send(None)
        return dec_cr
    return init_cr

@safety_wrapper
def summer_v3(total_init=0):
    total = total_init
    while True:
        z = yield total
        total += z if z else 0

summer = summer_v3()
assert summer.send(10) == 10

Note in the example we are not using next instead we are using obj.send(None). Using next is actually calling obj.send(None) internally.

How to stop or rest summer for summing up.

Coroutine can be closed just like a generator which has been exhausted. Coroutine can be auto closed when it goes out of scope. Clean up action can be taken when coroutine by catching GeneratorExit exception.

 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
27
28
29
30
31
32
33
class Reset(Exception):
    pass

@safety_wrapper
def summer_v4(total_init=0):
    total = total_init
    while True:
        try:
            z = yield total
            total += z if z else 0
        except Reset:
            total = 0
        except GeneratorExit:
            print(f"total={total}")
            return

summer = summer_v4()
assert summer.send(10) == 10
summer.close()  # prints `total=10`

exception = False
try:
    summer.send(11)
except StopIteration:
    exception = True

assert exception

summer_1 = summer_v4()
assert summer_1.send(10) == 10
assert summer_1.send(10) == 20
assert summer_1.throw(Reset) == 0
assert summer_1.send(10) == 10

The coroutine is suppose return on GeneratorExit marking end of generator. If the coroutine return then RuntimeError: generator ignored GeneratorExit is raised. On return the coroutine has ended hence it will throw StopIteration if send is called.

The coroutine also has a method throw which can be used to throw exception. This interface is used throw Reset exception which reset the state to 0.

Post Tags:  #python,  #coroutine