协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。
1.1、用作协程的生成器的基本行为
>>> 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'
关键在于协程在yield
关键字所在的位置暂停执行:在赋值语句中,=
右边的代码在赋值之前执行。因此,对于b = yield a
这行代码来说,等到客户端代码再次激活协程时才会设定b
的值。这便是异步编程中yield
的作用
1.2、协程使用示例:移动平均值的计算
1.2.1 类方法实现
>>> 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 高阶函数闭包实现
实现一:
>>> 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]
实现二:
>>> 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()
函数中使用了nonlocal
声明。因为当为自由变量赋值时,会隐式创建局部变量,例如:count = count + 1
,会隐式创建局部变量count
。这样,count
就不是自由变量,也不会保存在闭包中了。使用nonlocal
声明的变量可以避免上述问题。
1.2.3 协程方法实现
实现一:无返回值
>>> 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'
实现二:有返回值
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)
"""
获取协程返回值要绕一个圈子。yield from
结构会在内部自动捕获StopIteration
异常,还会把value
属性的值变成yield from
表达式的值。
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)