Python Coroutines - Smooth Python

A coroutine is a process that cooperates with a caller to produce a value provided by the caller.

1.1. Basic behavior of generators used as coroutines

>>> def simple_coro2(a):
...     print('-> Started: a =', a)
...     b = yield a
...     print('-> Received: b =', b)
...     c = yield a + b
...     print('-> Received: c =', c)
... 
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2)
'GEN_CREATED'                             # inspect.getgeneratorstate 函数指明 GEN_CREATED 状态.
>>> next(my_coro2)                        # 激活协程,协程向前执行到第一个 yield 表达式,
-> Started: a = 14                        # 打印 
14                                        # 产出 a 的值
>>> getgeneratorstate(my_coro2)
'GEN_SUSPENDED'                           # GEN_SUSPENDED 状态
>>> my_coro2.send(28)                     # 将数字 28 发送给暂停的协程;计算 yield 表达式,得到 28,
-> Received: b = 28                       # 并将其绑定给 b. 打印
42                                        # 产出 a + b,然后暂停,等待为 c 赋值.
>>> my_coro2.send(99)                     # 将数字 99 发送给暂停的协程;计算 yield 表达式,得到 99,
-> Received: c = 99                       # 并将其绑定给 c. 打印
Traceback (most recent call last):        # 协程终止, 生成器对象抛出 StopIteration 异常.
  File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2)           # 指明协程处于 GEN_CLOSED 状态.
'GEN_CLOSED'    

The point is that the coroutine yieldpauses execution at the point of the keyword: in an assignment statement, =the code on the right is executed before the assignment. So, for b = yield athis line of code, the value that won't be set until the client code activates the coroutine again b. yieldThis is what happens in asynchronous programming

1.2. Example of coroutine use: calculation of moving average

1.2.1 Class method implementation

>>> class Averager():
...     
...     def __init__(self):
...         self.series = []
...     
...     def __call__(self, new_value):
...         self.series.append(new_value)
...         total = sum(self.series)
...         return total/len(self.series)
... 
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

1.2.2 Implementation of higher-order function closures

Implementation one:

>>> def make_averager():
...     series = []                   # series 是 make_averager 函数的局部变量
...     
...     def averager(new_value):
...         series.append(new_value)  # series 是 自由变量(未在本地作用域中绑定的变量)
...         total = sum(series)
...         return total/len(series)
...     
...     return averager
... 
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
>>> avg.__code__.co_varnames         # avg 的局部变量
('new_value', 'total')
>>> avg.__code__.co_freevars         # avg 的自由变量
('series',)
>>> avg.__closure__                  # series 的绑定在返回的 avg 函数的 __closure__ 属性中。
                                     # avg.__closure__ 中的各个元素对应于 avg.__code__.co_freevars
                                     # 中的一个名称。
(<cell at 0x7f06f930ac18: list object at 0x7f06f89d1e08>,)
>>> avg.__closure__[0].cell_contents # 这些元素是 cell 对象,有个 cell_contents 属性,保存这真正的值。
[10, 11, 12]

Implementation two:

>>> def make_averager():
...     total = 0
...     count = 0
...     
...     def averager(new_value):
...         nonlocal count, total    # nonlocal 声明,把变量标记为自由变量。
...         count += 1               # 为nonlocal声明的变量赋予新值,闭包中保存的绑定会更新。
...         total += new_value
...         return total/count
...     
...     return averager
... 
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
>>> avg.__code__.co_varnames
('new_value',)
>>> avg.__code__.co_freevars
('count', 'total')
>>> avg.__closure__
(<cell at 0x7f06f930ac18: int object at 0xa69e40>, <cell at 0x7f06f897b348: int object at 0xa6a200>)
>>> avg.__closure__[0].cell_contents
3
>>> avg.__closure__[1].cell_contents
33
>>> 

averager()nonlocalA declaration is used in a function . Because when assigning a value to a free variable, a local variable is created implicitly, eg: count = count + 1, a local variable is created implicitly count. That way, countit's not a free variable, and it's not stored in a closure. Using nonlocaldeclared variables avoids the above problems.

1.2.3 Coroutine method implementation

Implementation 1: no return value

>>> def averager():
...     total = 0.0
...     count = 0
...     average = None
...     
...     while True:
...         term = yield average
...         total += term
...         count += 1
...         average = total/count
... 
>>> coro_avg = averager()
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(coro_avg)
'GEN_CREATED'
>>> next(coro_avg)
>>> coro_avg.send(10)
10.0
>>> coro_avg.send(11)
10.5
>>> coro_avg.close()
>>> getgeneratorstate(coro_avg)
'GEN_CLOSED'

Implementation 2: There is a return value

from collections import namedtuple
from functools import wraps

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


def coroutine(func):
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return primer


@coroutine
def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        if term is None:
            break
        total += term
        count += 1
        average = total/count
    return Result(count, average)
"""
>>> coro_avg = averager()
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(coro_avg)
'GEN_SUSPENDED'
>>> coro_avg.send(10)
10.0
>>> coro_avg.send(20)
15.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(None)                      # 发送 None 终止循环,导致协程结束,返回结果
Traceback (most recent call last):           # 生成器对象会抛出 StopIteration 异常。
  File "<stdin>", line 1, in <module>        # 异常对象的 value 属性保存着返回值。
StopIteration: Result(count=3, average=20.0) #

"""
"""
>>> coro_avg = averager()
>>> getgeneratorstate(coro_avg)
'GEN_SUSPENDED'
>>> coro_avg.send(10)
10.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(6.5)
15.5
>>> try:
...     coro_avg.send(None)
... except StopIteration as exc:
...     result = exc.value            # 通过捕获 StopIteration 异常,获取 averager 返回值
... 
>>> result
Result(count=3, average=15.5) 

"""

Getting the return value of the coroutine is a circle. yield fromThe struct automatically catches StopIterationexceptions internally and also turns the value of the valueproperty into yield fromthe value of the expression.

from collections import namedtuple


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


# 子生成器
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 Result(count, average)


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],
}


# 委派生成器
def grouper(results, key):
    while True:
        results[key] = yield from averager() # grouper 在该处暂停,等待 averager 实例处理客户端发来的值。
                                             # averger 运行完毕后(客户端发送None),返回的值绑定到
                                             # results[key] 上
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
        ))

# 调用方
def main(data):
    results = {}
    for key, values in data.items():
        # group是调用grouper函数得到的生成器对象。group作为协程使用
        group = grouper(results, key)
        next(group)
        for value in values:
            # 把各个value传给grouper。传入的值最终到达averager函数中term = yield那一行
            group.send(value)    # 将 value 传送给 grouper。 传入的值最终到达 averager 函数
                                 # term = yield 行,grouper 永远不知道传入的值是什么
        # print(results)
        group.send(None)         # 必须发送 None ,否则 averager 子生成器永远不会终止,
                                 # 委派生成器 group 永远不会再次激活, 
                                 # 因此永远不会为 results[key] 赋值。当外层 for 循环
                                 # 重新迭代时会新建一个 grouper 实例并绑定到 group 变量上。
                                 # 前一个 grouper 实例以及它创建的尚未终止的 averager 子
                                 # 生成器实例被垃圾回收程序回收。
    # print(results)
    report(results)


if __name__ == '__main__':
    main(data)

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325807377&siteId=291194637