- 上一篇python协程1:协程 10分钟入门介绍了:生成器作为协程使用时的行为和状态
- 使用装饰器预激协程
- 调用方如何使用生成器对象的 .throw(...) 和 .close() 方法控制协程
这一篇将介绍:
- 协程终止时如何返回值
- yield新句法的用途和语义
让协程返回值
先看一个例子:
这段代码会返回最终均值的结果,每次激活协程时不会产出移动平均值,而是最后一次返回。
我们调用这段代码,结果如下
return 表达式的值会传给调用方,赋值给StopIteration 异常的一个属性。这样做虽然看着别扭,但为了保留生成器对象耗尽时抛出StopIteration异常的行为,也可以理解。
如果我们想获取协程的返回值,可以这么操作:
看到这我们会说,这是什么鬼,为什么获取返回值要绕这么一大圈,就没有简单的方法吗?
有的,那就是 yield from
yield from 结果会在内部自动捕获StopIteration 异常。这种处理方式与 for 循环处理StopIteration异常的方式一样。
对于yield from 结构来说,解释器不仅会捕获StopIteration异常,还会把value属性的值变成yield from 表达式的值。
在函数外部不能使用yield from(yield也不行)。
既然我们提到了 yield from 那yield from 是什么呢?
yield from
yield from 是 Python3.3 后新加的语言结构。和其他语言的await关键字类似,它表示:*在生成器 gen 中使用 yield from subgen()时,subgen 会获得控制权,把产出的值传个gen的调用方,即调用方可以直接控制subgen。于此同时,gen会阻塞,等待subgen终止。
yield from 可用于简化for循环中的yield表达式。
例如:
可以改写为:
下面来看一个复杂点的例子:(来自Python cookbook 3 ,github源码地址 https://github.com/dabeaz/python-cookbook/blob/master/src/4/how_to_flatten_a_nested_sequence/example.py)
yield from x 表达式对x对象做的第一件事是,调用 iter(x),获取迭代器。所以要求x是可迭代对象。
PEP380 的标题是 ”syntax for delegating to subgenerator“(把指责委托给子生成器的句法)。由此我们可以知道,yield from是可以实现嵌套生成器的使用。
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,使两者可以直接发送和产出值,还可以直接传入异常,而不用在中间的协程添加异常处理的代码。
yield from 包含几个概念:
- 委派生成器
包含yield from <iterable> 表达式的生成器函数
- 子生成器
从yield from <iterable> 部分获取的生成器。
- 调用方
调用委派生成器的客户端(调用方)代码
这个示意图是 对yield from 的调用过程
委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给字生成器,子生成器再把产出的值发送给调用方。子生成器返回之后,解释器会抛出StopIteration异常,并把返回值附加到异常对象上,只是委派生成器恢复。
这个图来自于PaulSokolovsky 的 How Python 3.3 yield from construct works
下边这个例子是对yield from 的一个应用:
这段代码从一个字典中读取男生和女生的身高和体重。然后把数据传给之前定义的 averager 协程,最后生成一个报告。
执行结果为
这断代码展示了yield from 结构最简单的用法。委派生成器相当于管道,所以可以把任意数量的委派生成器连接在一起---一个委派生成器使用yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个生成器。最终以一个只是用yield表达式的生成器(或者任意可迭代对象)结束。
yield from 的意义
PEP380 分6点说明了yield from 的行为。
- 子生成器产出的值都直接传给委派生成器的调用方(客户端代码)
- 使用send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是None,那么会调用子生成器的 next()方法。如果发送的值不是None,那么会调用子生成器的send()方法。如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
- 生成器退出时,生成器(或子生成器)中的return expr 表达式会触发 StopIteration(expr) 异常抛出。
- yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。
- 传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的throw()方法。如果调用throw()方法时抛出 StopIteration 异常,委派生成器恢复运行。StopIteration之外的异常会向上冒泡。传给委派生成器。
- 如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用close() 方法,那么在子生成器上调用close() 方法,如果他有的话。如果调用close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常。
yield from的具体语义很难理解,不过我们可以看下Greg Ewing 的伪代码,通过伪代码分析一下:
上段代码变量说明:
- _i 迭代器(子生成器)
- _y 产出的值 (子生成器产出的值)
- _r 结果 (最终的结果 即整个yield from表达式的值)
- _s 发送的值 (调用方发给委派生成器的值,这个只会传给子生成器)
- _e 异常 (异常对象)
我们可以看到在代码的第一个 try 部分 使用 y = next(i) 预激了子生成器。这可以看出,上一篇我们使用的用于自动预激的装饰器与yield from 语句不兼容。
除了这段伪代码之外,PEP380 还有个说明:
这也就是为什么 yield from 可以使用return 来返回值而 yield 只能使用 try ... except StopIteration ... 来捕获异常的value 值。
到这里,我们已经了解了 yield from 的具体细节。下一篇,会分析一个使用协程的经典案例: 仿真编程。这个案例说明了如何使用协程在单线程中管理并发活动。
参考文档
- 流畅的python 第16章(这是读书笔记,这是读书笔记)
- PEP 380-- Syntax for Delegating to a Subgenerator:https://www.python.org/dev/peps/pep-0380/#proposal
- How Python 3.3 "yield from" construct works:http://flupy.org/resources/yield-from.pdf)