WHCSRL 技术网

流畅的python笔记(十六)协程_chk

目录

一、生成器如何进化成协程

二、用作协程的生成器的基本行为

三、使用协程计算移动平均值 

四、预激协程的装饰器

五、终止协程和异常处理

发送哨符值让协程退出

使用throw和close显式把异常发给协程

generator.throw(exc_type[, exc_value[, traceback]])

generator.close()

使用close和throw控制协程例子

六、让协程返回值

七、使用yield from

八、yield from的意义

yield from六点行为

yield from与异常处理


一、生成器如何进化成协程

python2.5以后,yield关键字可以在表达式中使用,而且生成器API增加了.send(value)方法。生成器的调用方可以使用.send()方法发送数据,发送的数据会成为生成器函数中yield表达式的值。因此生成器可以作为协程使用。协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值

        除了.send()方法外,还添加了.throw()和.close()方法:

  • send()方法,调用方使用send方法发送数据,该数据会成为生成器函数中yield表达式的值。
  • throw方法,让调用方抛出异常,在生成器中处理。
  • close()方法,终止生成器。

python3.3中为了更好地作为协程使用,对生成器句法做了两处改动:

  • 现在,生成器可以返回一个值,以前如果在生成器中给return语句提供值,会抛出SyntaxError异常。
  • 新引入了yield from句法,使用它可以把复杂的生成器重构成小型的嵌套生成器,省去了之前把生成器的工作委托给子生成器所需的大量代码。

二、用作协程的生成器的基本行为

  1. 协程使用生成器函数定义:定义体中有yield关键字。
  2. yield在表达式中使用,如果协程只需要从客户那里接收数据,那么yield产出的值是None,这个值是隐式指定的,因为yield关键字右边没有表达式。也就是说,yield表达式右边才是生成器要产出的值,如果右边没有表达式,则相当于隐式指定产出None。
  3. 调用生成器函数得到生成器对象。
  4. 首先要调用next()函数,因为生成器还没启动,没在yield语句处暂停,所以一开始无法发送数据。
  5. 生成器对象调用send方法,参数即为调用者要发送的数据,本例中协程定义体中的yield表达式会计算出42,调用完send方法之后,协程会恢复,一直运行到下一个yield表达式,或者终止。
  6. 控制权流动到协程定义体的末尾,导致生成器像往常一样抛出StopIteration异常。

协程可以身处四个状态中的一个,当前状态可以使用inspect.getgeneratorstate()函数确定,该函数会返回以下字符串中的一个:

  • ‘GEN_CREATED’,等待开始执行。
  • ‘GEN_RUNNING’,解释器正在执行。
  • 'GEN_SUSPENDED',在yield表达式处暂停。
  • 'GEN_CLOSED',执行结束。

因为send方法的参数会成为暂停的yield表达式的值,所以仅当协程处于暂停状态时才能调用send方法,例如my_coro.send(42),但是,如果协程还没激活(即处于GEN_CREATED"状态),则会发生错误。因此在send真正的数据之前必须要先激活协程,有两种方法激活协程:

  1. 调用next(my_coro)。
  2. 调用my_coro.send(None),即发送None值给协程对象。

如果创建协程之后立即把None之外的值发给它,会出现以下错误:

预激协程:即最先调用next(my_coro)函数这一步,让协程向前执行到第一个yield表达式,准备好作为活跃的协程使用。

        上边例子协程只产出了一个值,下边例子协程产出两个值:

  1. inspect.getgeneratorstate函数指明,协程未启动,即处于GEN_CREATED状态。
  2. 预激协程,即向前执行协程到第一个yield表达式,打印 -> Started: a = 14,然后yield a产出a的值(14),并且暂停,等待为b赋值。
  3. getgenetatorstate函数指明,协程处于GEN_SUSPENDED状态,即协程在yield表达式处暂停。
  4. 把数字28发送给暂停的协程,计算yield表达式得到28,然后把28绑定给b。然后协程继续执行,打印消息,执行到第二个yield处,产出 a + b的值42,然后协程再次暂停,等待调用者发送数据并赋值给c。
  5. 把数字99发给暂停的协程,此时计算yield表达式得到99,然后把99绑定给c,继续执行协程。因为下边已经没有yield了,因此执行到协程终止,导致生成器抛出StopIteration异常。
  6. getgeneratorstate函数指明,协程执行结束,处于GEN_CLOSED状态。

关键一点是,协程在yield关键字所在的位置暂停执行。在赋值语句中,=右边的代码在赋值之前之宗,因此,对于 b = yield a这行代码来说,等到客户端代码再激活协程时才会给b赋值。

三、使用协程计算移动平均值 

  1. 这个无线循环表明,只要调用方不断把值发给这个协程,他就会一直收值,然后生成结果。仅当调用方在协程中调用.close()方法,或者没有对协程的引用而被垃圾回收程序回收时,这个协程才会终止。
  2. yield表达式用于暂停执行协程,把结果发给调用方;还用于接收调用方后面发给协程的值,恢复无线循环。

使用协程的好处是total和count声明为局部变量即可,无需使用实例属性或闭包在多次调用之间保持上下文。

  1. 创建协程对象。
  2. 调用next函数,预激协程。
  3. 计算移动平均值,多次调用.send()方法,产出当前的平均值。

调用next(coro_avg)预激协程后,协程会向前执行到yield表达式,产出average变量的初始值------None,因此不会出现在控制台中。

四、预激协程的装饰器

如果不预激,则协程没什么用。因为调用my_coro.send(x)之前,一定要调用next(my_coro)。当时很多时候容易忘记预激,为了简化协程的用法,有时会使用一个预激装饰器,比如coroutine

        下面是装饰器coroutine的源码,@wraps是python内置的一个装饰器,其通常放在装饰器的源码中用来修饰内函数,其作用是消除装饰器装饰过函数以后,被装饰函数的函数名等属性被修改的副作用。

  1. 把被装饰的生成器函数替换成这里的primer函数;调用primer函数,返回预激后的生成器。
  2. 调用被装饰的函数,获取生成器对象。
  3. 预激生成器。
  4. 返回生成器。

下面用@coroutine装饰器定义并测试计算移动平均值的协程。

  1. 调用average生成器函数创建一个生成器对象,在coroutine装饰器的primer函数中已经预激了这个生成器。
  2. getgeneratorstate函数表明,协程处于GEN_SUSPENDED状态,因此协程已经准备好可以接收值了。
  3. 可以立即把值发送给coro_avg,这正是coroutine装饰器的作用,防止调用者忘记预激协程。
  4. 导入coroutine装饰器。
  5. 把装饰器应用到averager函数上。
  6. 函数定义体与上一个版本完全一样。

五、终止协程和异常处理

协程中未处理的异常会向上冒泡,传给next函数或send方法的调用方(即触发协程的对象)。如下案例:

  1.  使用@coroutine装饰器装饰的averager协程,可以立即开始发送值。
  2.  发送的值不是数字,导致协程内部有异常抛出。
  3.  由于在协程内没有处理异常,协程会终止,如果试图重新激活协程,会抛出StopIteration异常。

出错的原因是,发送给协程的'spam'值不能加到total变量上。

发送哨符值让协程退出

内置的None和Ellipsis等常量经常用作哨符值。Ellipsis的优点是,数据流中不太常有这个值。还可以把StopIteration类本身(不是实例)作为哨符值,即my_coro.send(StopIteration)。

使用throw和close显式把异常发给协程

generator.throw(exc_type[, exc_value[, traceback]])

致使生成器在暂停的yield表达式处抛出指定的异常。

  • 如果生成器处理了抛出的异常,代码向前执行到下一个yield表达式,而产出的值会成为调用generator.throw方法得到的返回值。
  • 如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。

generator.close()

致使生成器在暂停的yield表达式处抛出GeneratorExit异常。

  • 如果生成器没有处理这个异常,或者抛出了StopIteration异常(通常指运行到结尾),调用方不会报错。
  • 如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。
  • 生成器抛出的其他异常会向上冒泡,传给调用方。

使用close和throw控制协程例子

 

  1. 特别处理DemoException异常。
  2. 如果没有异常,那么显示接收到的值。
  3. 这一行永远不会执行。因为只有未处理的异常才会中止无限循环,而一旦出现未处理的异常,协程会立即终止。

没有异常时,协程demo_exc_handling使用情况如下:

 

把异常DemoException传入demo_exc_handling(用throw)不会导致协程中止:

可以看到如果传入协程的异常得到处理,则协程状态变成'GEN_SUSPENDED',即协程在yield表达式处暂停。此时应该可以由调用发继续给协程发送值。

        但是,如果传入协程的异常没有处理,协程会停止,即状态变成‘GEN_CLOSED’,如下:

 如果不管协程如何结束都想做些清理工作,要把协程定义体中相关的代码放入try/finally块中:

六、让协程返回值

如下例子中,每次迭代时协程不会产出值,而是在最后返回一个namedtuple,具名元组有两个字段,分别时项数count和平均值average。

  1.  为了返回值,协程必须正常终止,因此这一版中用一个条件判断来退出累计循环。
  2.  返回一个具名元组,包含count和average两个字段,在python3.3之前,如果生成器返回值,解释器会报语法错误。

  1.  这一版不产出值。
  2.  发送None会终止循环,导致协程结束,返回最终结果。协程结束会抛出StopIteration异常,异常对象的value属性保存着返回值。

在协程中,return表达式的值会传给调用方,赋值给StopIteration异常的一个属性,这样做有点不太合理,但是能保留生成器对象的常规行为------耗尽时抛出StopIteration异常。可以直接捕获这个异常,获取协程返回的值:

以上获取协程返回值要绕个圈子,而使用yield from结构会在内部自动捕获StopIteration异常,这种处理方式与for循环处理StopIteration异常的方式一样,对yield from结构来说,解释器不仅会捕获StopIteration异常,还会把value属性的值变成yield from表达式的值。但是我们无法在控制台测试这种行为,因为函数外部使用yield from 以及yield会导致语法错误。

七、使用yield from

使用yield from简化for循环中的yield表达式:

使用yield from可以链接可迭代的对象:

综上可以看出,yield后边跟的是迭代时实际产出的对象,而yield from后面跟的是可迭代对象,要产出的是可迭代对象中的元素。yield from x表达式对x对象做的第一件事就是调用iter(x)获取迭代器,因此x可以是任何可迭代对象(只要实现了iter方法就是可迭代对象)。

        但yield from结构的作用不止是替代产出值得for循环,而要发散思维,使用嵌套的生成器,yield from结构是把职责委托给子生成器的句法。yield from的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的代码。

        为了使用yield from,PEP380使用了一些专门术语:

  • 委派生成器,包含yield from <iterable>表达式的生成器函数。
  • 子生成器,从yield from 表达式中<iterable>部分获得的生成器。
  • 调用方,调用了委派生成器的客户端代码。有些语境中等价于客户端。

下图展示调用方、委派生成器、和子生成器各自的使用:

委派生成器在yield from表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回(迭代结束)之后,解释器会抛出StopIteration异常,并把返回值附加到异常对象上,此时委派生成器会恢复。

        以下示例说明yield from结构的用法:

 

 

  1.  averager协程,用于计算移动平均值,返回一个具名元组,这里作为子生成器使用。
  2.  main函数中的客户代码发送的各个值绑定到这里的term变量上。
  3.  终止条件,如果没有这个,则使用yield from调用这个协程的生成器会永远阻塞。
  4.  协程的返回值,返回的Result会成为grouper函数中yield from表达式的值。
  5.  grouper是委派生成器。
  6.  这个循环每次迭代时都会创建一个新的averager实例,每个实例都是作为协程使用的生成器对象。
  7.  通过grouper发送的每个值,都会经过yield from处理,通过管道传给averager实例。grouper会在yield from表达式处暂停,等待averager实例处理客户端发来的值(即客户端通过grouper.send来给averager中的yield语句传值)。averager实例运行完毕后,返回的值绑定到results[key]上。
  8. main函数是客户端代码,是调用方。
  9.  group是调用grouper函数得到的生成器对象,传给grouper函数的第一个参数是results,用于手机结果;第二个参数是某个键。group作为协程使用。
  10.  预激group协程。
  11.  把各个value传给grouper,传入的值最终到达averager函数中,用来给term赋值,grouper永远都不知道传入的值是什么。
  12.  把None传入grouper,导致当前的averager实例终止,也让grouper继续运行,再创建一个averager实例,处理下一组值。

下面简要说明上例运作方式:

  • 外层for循环每次迭代会新键一个grouper实例,赋值给group变量,group是委派生成器。
  • 调用next(group),预激委派生成器group,此时进入while True循环,调用子生成器averager后,group在yield from表达式处暂停。
  • 内层for循环调用group.send(value),直接把值传给子生成器averager。同时当前的grouper实例group在yield from处暂停。
  • 内层循环结束后,group实例依旧在yield from表达式处暂停,因为averager实例还没有结束,因此,grouper函数体中未results[key]赋值的语句还没有执行。
  • 如果外层for循环的末尾没有group.send(None),那么averager子生成器就永远不会终止,委培生成器group永远不会再次激活(会一直暂停在yield from),因此永远不会未results[key]赋值。
  • 外层for循环重新迭代时会新键一个grouper实例,然后绑定到group变量上,前一个grouper实例(以及它创建的尚未终止的averager子生成器实例)被垃圾回收程序回收。

这个试验表明,如果子生成器不终止,委派生成器会在yield from表达式处永远暂停,如果是这样,程序不会向前执行,因为yield from和yield一样把控制权转交给了客户代码(委派生成器的调用方)。

        上例展示了yield from结构最简单的用法,只有一个委派生成器和一个子生成器,因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用yield from调用一个子生成器,子生成器本身也是委培生成器,使用yield from调用另一个子生成器,依次类推。最终,这个链条要以一个只使用yield表达式的简单生成器结束(也可以以任意可迭代对象结束)。

        任何yield from链条都必须由客户驱动,在最外层委派生成器上调用next()函数或.send()方法,可以隐式调用,例如使用for循环。

八、yield from的意义

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异常。

 yield from与异常处理

  1.  EXPR可以是任何可迭代的对象,因为获取迭代器_i(这是子生成器)使用的是iter()函数。
  2.  预激子生成器,结果保存在_y中,作为产出的第一个值。
  3.  如果抛出StopIteration异常,获取异常对象的value属性,赋值给_r(这是最简单情况下的返回值RESULT)。
  4.  运行这个循环,委派生成器会阻塞,只作为调用方和子生成器之间的通道。
  5. 产出子生成器当前产出的元素;等待调用方发送_s中保存的值。
  6. 尝试让子生成器向前执行,转发调用方发送的_s。
  7. 如果子生成器抛出StopIteration异常,获取value属性的值,赋值给_r,然后退出循环,让委派生成器恢复运行。
  8. 返回的结果RESULT是_r,即整个yield from表达式的值。

 

推荐阅读