Python教程057 协程与生成器2
文章目录
示例 定义一个计算移动平均值的协程
|
|
➊ 这个无限循环表明,只要调用方不断把值发给这个协程,它就会一直接收值,然后生 成结果。仅当调用方在协程上调用 .close() 方法,或者没有对协程的引用而被垃圾回收 程序回收时,这个协程才会终止。
➋ 这里的 yield 表达式用于暂停执行协程,把结果发给调用方;还用于接收调用方后面 发给协程的值,恢复无限循环。 使用协程的好处是,total 和 count 声明为局部变量即可,无需使用实例属性或闭包在 多次调用之间保持上下文。示例 16-4 是使用 averager 协程的 doctest。
示例 定义的移动平均值协程的 doctest
|
|
❶ 创建协程对象。 ❷ 调用 next 函数,预激协程。 ❸ 计算移动平均值:多次调用 .send(…) 方法,产出当前的平均值。
在上述doctest中,调用next(coro_avg)函数后,协程会向前执行到yield表达式,产出average变量的初始值——None,因此不会出现在控制台中。此时,协程在yield表达式处暂停,等到调用方发送值。coro_avg.send(10) 那一行发送一个值,激活协程,把发送的值赋给yield average表达式,也就是term,并更新total、count和average三个变量的值,然后开始while循环的下一次迭代,产出average变量的值,等待下一次为term变量赋值。
如何启动协程。使用协程之前必须预激,可是这一步容易忘记。为了避免忘记,可以在协程上使用一个特殊的装饰器。
预激协程的装饰器
如果不预激,那么协程没什么用。调用my_coro.send(x) 之前,记住一定要调用next(my_coro)。为了简化协程的用法,有时会使用一个预激装饰器。
示例 预激协程的装饰器
|
|
❶ 把被装饰的生成器函数替换成这里的 primer 函数;调用 primer 函数时,返回预激后的生成器。
❷ 调用被装饰的函数,获取生成器对象。
❸ 预激生成器。
❹ 返回生成器。
示例 展示@coroutine 装饰器的用法。
|
|
❶ 导入 coroutine 装饰器。
❷ 把装饰器应用到 averager 函数上。
❸ 调用 averager() 函数创建一个生成器对象,在 coroutine 装饰器的 primer 函数中 已经预激了这个生成器。
❹ getgeneratorstate 函数指明,处于 GEN_SUSPENDED 状态,因此这个协程已经准备 好,可以接收值了。
❺ 可以立即开始把值发给 coro_avg——这正是 coroutine 装饰器的目的。
终止协程和异常处理
协程中未处理的异常会向上冒泡,传给 next 函数或 send 方法的调用方(即触发协程的 对象)。
示例 未处理的异常会导致协程终止
|
|
❶ 使用 @coroutine 装饰器装饰的 averager 协程,可以立即开始发送值。
❷ 发送的值不是数字,导致协程内部有异常抛出。
❸ 由于在协程内没有处理异常,协程会终止。如果试图重新激活协程,会抛出StopIteration异常。出错的原因是,发送给协程的’spam’值不能加到 total 变量上。
示例暗示了终止协程的一种方式:发送某个哨符值,让协程退出。内置的 None 和 Ellipsis 等常量经常用作哨符值。Ellipsis 的优点是,数据流中不太常有这个值。我 还见过有人把 StopIteration类(类本身,而不是实例,也不抛出)作为哨符值;也就是说,是像这样使用的:my_coro.send(StopIteration)。 从Python 2.5 开始,客户代码可以在生成器对象上调用两个方法,显式地把异常发给协程。 这两个方法是 throw 和 close。
generator.throw(exc_type[, exc_value[, traceback]])
致使生成器在暂停的 yield 表达式处抛出指定的异常。如果生成器处理了抛出的异 常,代码会向前执行到下一个 yield 表达式,而产出的值会成为调用 generator.throw 方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。
generator.close()
致使生成器在暂停的 yield 表达式处抛出 GeneratorExit 异常。如果生成器没有处 理这个异常,或者抛出了 StopIteration 异常(通常是指运行到结尾),调用方不会报 错。如果收到 GeneratorExit 异常,生成器一定不能产出值,否则解释器会抛出 RuntimeError 异常。生成器抛出的其他异常会向上冒泡,传给调用方。
下面举例说明如何使用 close 和 throw 方法控制协程。 示例 coro_exc_demo.py:学习在协程中处理异常的测试代码
|
|
❶ 特别处理 DemoException 异常。
❷ 如果没有异常,那么显示接收到的值。
❸ 这一行永远不会执行。
示例中的最后一行代码不会执行,因为只有未处理的异常才会中止那个无限循环, 而一旦出现未处理的异常,协程会立即终止。
示例 激活和关闭 demo_exc_handling,没有异常
|
|
如果把 DemoException 异常传入 demo_exc_handling 协程,它会处理,然后继续运行,如下面示例 所示
示例 把DemoException异常传入demo_exc_handling 不会导致协程中止
|
|
但是,如果传入协程的异常没有处理,协程会停止,即状态变成 ‘GEN_CLOSED’。下面的示例 演示了这种情况。
示例 如果无法处理传入的异常,协程会终止
|
|
如果不管协程如何结束都想做些清理工作,要把协程定义体中相关的代码放入 try/finally 块中,如示例。
示例 coro_finally_demo.py:使用 try/finally 块在协程终止时执行操作
|
|
让协程返回值
下面的示例 是 averager 协程的不同版本,这一版会返回结果。为了说明如何返回值,每次激活协程时不会产出移动平均值。这么做是为了强调某些协程不会产出值,而是在最后 返回一个值(通常是某种累计值)。
示例中的 averager协程返回的结果是一个 namedtuple,两个字段分别是项数 (count)和平均值(average)。我本可以只返回平均值,但是返回一个元组可以获得 累积数据的另一个重要信息——项数。
示例 coroaverager2.py:定义一个求平均值的协程,让它返回一个结果
|
|
➊ 为了返回值,协程必须正常终止;因此,这一版 averager 中有个条件判断,以便退出累计循环。
➋ 返回一个 namedtuple,包含 count 和 average 两个字段。在 Python 3.3 之前,如果生成器返回值,解释器会报句法错误。
下面在控制台中说明如何使用新版 averager,如示例所示。
示例 coroaverager2.py:说明 averager 行为的 doctest
|
|
❶ 这一版不产出值。
❷ 发送 None 会终止循环,导致协程结束,返回结果。一如既往,生成器对象会抛出 StopIteration 异常。异常对象的 value 属性保存着返回的值。
注意,return 表达式的值会偷偷传给调用方,赋值给 StopIteration 异常的一个属 性。这样做有点不合常理,但是能保留生成器对象的常规行为——耗尽时抛出 StopIteration 异常。
示例 捕获 StopIteration 异常,获取 averager 返回的值
|
|
上面获取协程的返回值要绕个圈子,所以PEP 380引入了全新的语言结构yield from。yield from 结构会在内部自动捕获 StopIteration 异常。这种处理方 式与 for 循环处理 StopIteration 异常的方式一样:循环机制使用用户易于理解的方式处理异常。对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把 value属性的值变成yield from表达式的值。可惜,我们无法在控制台中使用交互的方 式测试这种行为,因为在函数外部使用 yield from(以及 yield)会导致句法出错。
Python 3.3 引入 yield from结构的主要原因之一与把异常传入嵌套的协程有关。另一个原因是让协程更方便地返回值。
使用yield from
首先要知道,yield from 是全新的语言结构。它的作用比 yield 多很多,因此人们认为 继续使用那个关键字多少会引起误解。在生成器gen中使用yield from subgen()时,subgen会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。与此同时,gen会阻塞,等待subgen终止。
yield from 可用于简化 for 循环中的 yield 表达式。例如:
|
|
可以改写为:
|
|
yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因 此,x 可以是任何可迭代的对象。
可是,如果 yield from 结构唯一的作用是替代产出值的嵌套 for 循环,这个结构很有 可能不会添加到 Python 语言中。yield from 结构的本质作用无法通过简单的可迭代对象 说明,而要发散思维,使用嵌套的生成器。因此,引入 yield from 结构的 PEP 380 才起 了“Syntax for Delegating to a Subgenerator”(“把职责委托给子生成器的句法”)这个标题。 yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起 来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中 添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职 责。
若想使用 yield from 结构,就要大幅改动代码。为了说明需要改动的部分,PEP 380 使用了一些专门的术语。 委派生成器 包含 yield from iterable 表达式的生成器函数。
子生成器 从 yield from 表达式中iterable 部分获取的生成器。这就是 PEP 380 的标题 (“Syntax for Delegating to a Subgenerator”)中所说的“子生成器”(subgenerator)。
调用方 PEP 380 使用“调用方”这个术语指代调用委派生成器的客户端代码。在不同的语境 中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,因为它调用了子 生成器)区分开。
示例 coroaverager3.py:使用 yield from 计算平均值并输出统计报告
|
|
❶ 与示例 16-13 中的 averager 协程一样。这里作为子生成器使用。
❷ main 函数中的客户代码发送的各个值绑定到这里的 term 变量上。
❸ 至关重要的终止条件。如果不这么做,使用 yield from 调用这个协程的生成器会永 远阻塞。
❹ 返回的 Result 会成为 grouper 函数中 yield from 表达式的值。
❺ grouper 是委派生成器。
❻ 这个循环每次迭代时会新建一个 averager 实例;每个实例都是作为协程使用的生成 器对象。
❼ grouper 发送的每个值都会经由 yield from 处理,通过管道传给 averager 实 例。grouper 会在 yield from 表达式处暂停,等待 averager 实例处理客户端发来的 值。averager 实例运行完毕后,返回的值绑定到 results[key] 上。while 循环会不断 创建 averager 实例,处理更多的值。
❽ main 函数是客户端代码,用 PEP 380 定义的术语来说,是“调用方”。这是驱动一切的 函数。
❾ group 是调用 grouper 函数得到的生成器对象,传给 grouper 函数的第一个参数是 results,用于收集结果;第二个参数是某个键。group 作为协程使用。
❿ 预激 group 协程。
⓫ 把各个 value 传给 grouper。传入的值最终到达 averager 函数中 term = yield 那 一行;grouper 永远不知道传入的值是什么。
⓬ 把 None 传入 grouper,导致当前的 averager 实例终止,也让 grouper 继续运行, 再创建一个 averager 实例,处理下一组值。
下面简要说明示例的运作方式,还会说明把 main 函数中调用 group.send(None) 那一行代码(带有“重要!”注释的那一行)去掉会发生什么事。
.外层for循环每次迭代会新建一个grouper实例,赋值给group变量;group 是委派生成器。
.调用next(group),预激委派生成器grouper,此时进入while True循环,调用子生成器averager后,在 yield from 表达式处暂停。
.内层for循环调用group.send(value),直接把值传给子生成器averager。同时,当前的grouper实例(group)在 yield from 表达式处暂停。
.内层循环结束后,group实例依旧在yield from表达式处暂停,因此,grouper函数定义体中为results[key] 赋值的语句还没有执行。
.如果外层 for 循环的末尾没有group.send(None),那么averager子生成器永远不会终止,委派生成器group永远不会再次激活,因此永远不会为 results[key] 赋值。
.外层for循环重新迭代时会新建一个grouper实例,然后绑定到group变量上。前一个grouper实例(以及它创建的尚未终止的 averager 子生成器实例)被垃圾回收程序回收。
Python-Coroutine2.PNG
图 把该示例中各个相关的部分 标识出来了。委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子 生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出 StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复
这个试验想表明的关键一点是,如果子生成器不终止,委派生成器会在 yield from 表达式处永远暂停。如果是这样,程序不会向前执行,因为 yield from(与 yield 一样)把控制权转交给客户代码(即,委派生成器的调用方)了。 显然,肯定有任务无法完成。
示例展示了 yield from 结构最简单的用法,只有一个委派生成器和一个子生成 器。因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用yield from调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器,以此类推。最终,这个链条要以一个只使用yield 表达式的简单生成器结束;不过,也能以任何可迭代的对象结束。 任何yield from链条都必须由客户驱动,在最外层委派生成器上调用next(…) 函数 或 .send(…) 方法。可以隐式调用,例如使用for循环。
下面综述 PEP 380 对 yield from 结构的正式说明。PEP 380 在“Proposal”一节https://www.python.org/dev/peps/pep-0380/#proposal 分六点说明了 yield from 的行为。
1)子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
2)使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的next() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
3)生成器退出时,生成器(或子生成器)中的return expr表达式会触发StopIteration(expr) 异常抛出。
4)yield from 表达式的值是子生成器终止时传给StopIteration 异常的第一个参数。
5)传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw() 方法。如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。StopIteration 之外的异常会向上冒泡,传给委派生成器。
6)如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么在子生成器上调用close()方法,如果它有的话。如果调用close()方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出GeneratorExit异常。
转载请注明本网址。