《流畅的Python》10-协程初步
协程是放在生成器,迭代器后面讲的,这也是生成器的最终的归宿,可以理解为高阶的特性。如果生成器仅仅是当作语法糖,那么它可以被很容易的被其他形式替代而不会被重视。
同时,作者指出,协程作为一种鲜为人知,资料匮乏的特性,看起来并不是很有用,常常被忽视。实际上关于Python的一般广为人知的特性已经介绍完了,不过事情正变得更有趣。
前面介绍协程,然后介绍新的句法,用yield from
来实现一个标准漂亮的协程。
协程的概念
从句法上来看,协程里有一个yield
关键字,意思是让步;产出
。协程看起来和生成器有点类似,最大的区别在于,它不但能读出数据,还能往里面推送数据。从根本上把 yield 视作控制流程的方式,这样就好理解协程了。
生成器作为协程时的行为和状态
这里要提出,强调的是生成器进化为协程后的行为。用 yield
关键字实现的生成器可以视作一个协程。
来一个简单的样例和图片来解释。
计算平均值,并打印
>>> def average():
count=0.0
total=0
average=0.0
term=yield
count+=term
total+=1
return count/total
>>> x=average()
>>> next(x)
>>> x.send(1)
1.0
>>> x.send(2)
1.5
>>> x.send(3)
2.0
>>> x.send(None)
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
x.send(None)
File "<pyshell#20>", line 7, in average
count+=term
TypeError: unsupported operand type(s) for +=: 'float' and 'NoneType'
>>>
- 创建后需要调用
next(gen)
来预激协程,效果等同于x.send(None)
- 协程结束后抛出
StopIterator
异常 - 程序进行到
yield
暂停,.send()
方法赋值给=
左边,与右边无关。
如下图过程的划分。
装饰器自动预激协程
使用一个装饰器,就不用每次开头都调用.next()
这个装饰器的作用就是调用一次next()
,并返回原来的函数(@wraps(func)
保证原来函数不会被修改)。
from functools import wraps
def coroutine(func):
"""装饰器:向前执行到第一个`yield`表达式,预激`func`"""
@wraps(func)
def primer(*args, **kwargs): ➊
gen = func(*args, **kwargs) ➋
next(gen) ➌
return gen ➍
return primer
终止协程和异常处理
- .close()方法即停止协程
- .throw()方法向协程内传递一个异常。
终止协程的一种方式:发送一个暗哨值
,让协程退出。
常见的暗哨值有None
,Ellipsis
,甚至还有StopIteration
处理异常记住一点:
协程内能被(try/finally)
处理的异常都能让协程正确进行,如果没有被处理,异常会向上级冒泡,传到调用方的上下文。
新句法! yield from
第 14 章说过,yield from 可用于简化 for 循环中的 yield 表达式。
例如
>>> def gen():
...
yield from 'AB'
...
yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]
但是yield fromr
远不止这种用法。
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,
这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。
有了这个结构,协程可以通过以前不可能的方式委托职责。
首先介绍三个术语:
- 委派生成器
- 子生成器
- 调用方
委派生成器
包含 yield from 表达式的生成器函数。
子生成器
从 yield from 表达式中 部分获取的生成器。
调用方
指代调用委派生成器的客户端代码。
实际上通过yield from
结构把调用方和(另一个)子生成器分开。而往往yield from
也会单独用一个结构比如函数定义。叫做委派生成器。委派生成器在传值过程中没有中间过程,直接联系两者。
一个有点复杂的例子。
from collections import namedtuple
Result = namedtuple('Result', 'count average')
def average():
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)
def grouper(results, key):
while(True):
results[key] = yield from average()
def main():
results = {}
for key, values in data.items():
group = grouper(results, key)
next(group)
for value in values:
group.send(value)
group.send(None)
report(results)
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],
}
if __name__ == '__main__':
main()
输出:
9 boys averaging 40.42kg
9 boys averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m
yield from 的意义
摘抄
- 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
- 使用
send()
方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的__next__()
方法。如
果发送的值不是None
,那么会调用子生成器的send()
方法。如果调用的方法抛出StopIteration
异常,那么委派生成器恢复运
行。任何其他异常都会向上冒泡,传给委派生成器。 - 生成器退出时,生成器(或子生成器)中的 return expr 表达式会触发 StopIteration(expr) 异常抛出。
- yield from 表达式的值是子生成器终止时传给 StopIteration异常的第一个参数。
yield from 结构的另外两个特性与异常和终止有关。
- 传入委派生成器的异常,除了
GeneratorExit
之外都传给子生成器的throw()
方法。如果调用throw()
方法时抛出StopIteration
异常,委派生成器恢复运行。StopIteration
之外的异常会向上冒泡,传给委派生成器。 - 如果把
GeneratorExit
异常传入委派生成器,或者在委派生成器上调用close()
方法,那么在子生成器上调用close()
方法,如果它有的话。如果调用close()
方法导致异常抛出,那么异常会
向上冒泡,传给委派生成器;否则,委派生成器抛出GeneratorExit
异常。
yield from 的伪代码
摘抄
即下面语句的伪代码。
RESULT = yield from EXPR
_i = iter(EXPR) ➊
try:
_y = next(_i) ➋
except StopIteration as _e:
_r = _e.value ➌
else:
while 1:➍
try:
_s = yield _y ➎
except GeneratorExit as _e:➏
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e
except BaseException as _e:➐
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else:➑
try:
_y = _m(*_x)
except StopIteration as _e:
_r = _e.value
break
else:➒
try:➓
if _s is None:⓫
_y = next(_i)
else:
_y = _i.send(_s)
except StopIteration as _e:⓬
_r = _e.value
break
RESULT = _r⓭
❶ EXPR 可以是任何可迭代的对象,因为获取迭代器 _i(这是子生成器)使用的是 iter() 函数。
❷ 预激子生成器;结果保存在 _y 中,作为产出的第一个值。
❸ 如果抛出 StopIteration 异常,获取异常对象的 value 属性,赋值给 _r——这是最简单情况下的返回值(RESULT)。
❹ 运行这个循环时,委派生成器会阻塞,只作为调用方和子生成器之间的通道。
❺ 产出子生成器当前产出的元素;等待调用方发送 _s 中保存的值。这个代码清单中只有这一个 yield 表达式。
❻ 这一部分用于关闭委派生成器和子生成器。因为子生成器可以是任何可迭代的对象,所以可能没有 close 方法。
❼ 这一部分处理调用方通过 .throw(...) 方法传入的异常。同样,子生成器可以是迭代器,从而没有 throw 方法可调用——这种情况会导致委派生成器抛出异常。
❽ 如果子生成器有 throw 方法,调用它并传入调用方发来的异常。子生成器可能会处理传入的异常(然后继续循环);可能抛出StopIteration 异常(从中获取结果,赋值给 _r,循环结束);还可能不处理,而是抛出相同的或不同的异常,向上冒泡,传给委派生成器。
❾ 如果产出值时没有异常......
❿ 尝试让子生成器向前执行......
⓫ 如果调用方最后发送的值是 None,在子生成器上调用 next 函数,否则调用 send 方法。
⓬ 如果子生成器抛出 StopIteration 异常,获取 value 属性的值,赋值给 _r,然后退出循环,让委派生成器恢复运行。
⓭ 返回的结果(RESULT)是 _r,即整个 yield from 表达式的值。
上一篇: 疯狗你骂谁
下一篇: 触碰jQuery:AJAX异步详解