1
导语:本文章记录了本人在学习Python基础之控制流程篇的重点知识及个人心得,打算入门Python的朋友们可以来一起学习并交流。

本文重点:

1、掌握协程的概念与行为;
2、掌握协程中的预激,终止和异常处理;
3、深入理解yield from的本质作用。

一、协程介绍

1、协程概述

协程:指的是与调用方协作,产出由调用方提供的值。
语法结构:协程是定义体中包含yield关键字的函数,一般使用生成器函数定义。
意义:协程中的yield关键字是一种控制流程工具。即不管数据如何流动,协程都会把控制权让步给中心调度程序,从而激活其他的协程实现协作式多任务。

2、协程的基本行为

协程包含四种状态:

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

可使用inspect.getgeneratorstate(...)查询协程所处的状态。

协程中重要的两个方法:

  • .send(datum):调用方把数据提供给协程。
  • next(coroutine):预激协程。

协程返回值:自Python3.3实现PEP 380以来对生成器函数做了两处改动,一处是生成器可以返回值。

3、实例1:协程初级使用——计算平均值

下面将利用协程计算用户传入若干数值的平均值。

def average():
    total=0.0
    number=0
    average=None
    while True:
        term=yield average
        total+=term
        number+=1
        average=total/number
        print(average)

process=average()
next(process)#预激协程
process.send(5)#输出5
process.send(10)#输出7.5
process.send(15) #输出10.0

小结:协程执行首先需要预激,使之准备好然后让步控制权。具体地说,协程在yield关键字所在的位置暂停执行。在term=yield average这个 赋值语句中,右边的代码会在赋值之前执行。 在暂停结束后,从先前阻塞的那行代码开始,将yield 表达式的值赋给左边的变量。

实例2:令协程返回值

from collections import namedtuple

Result = namedtuple('result','average count')

def average():
    total = 0.0
    number = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        number += 1
        average=total/number
    return Result(average,number) 

分析:当协程终止时,可以在return表达式中返回值。并且return表达式通过把值绑定到StopIteration的value属性上传给调用方返回值。事实上这也符合生成器的常规行为——耗尽时抛出StopIteration异常。

二、协程的预激、终止与异常处理

1、预激协程

协程在使用前须预激,让协程向前执行到第一个yield表达式,准备好作为活跃的协程使用。
预激的本质方法:

  • next(coroutine):常见的标准方法。

同时首次发送coroutine.send(None)也可以调用next(coroutine),实现相同功能,但缺乏可读性。

基于本质方法,我们衍生出自定义预激协程的装饰器的方法,避免忘记预激协程。
coroutine:预激协程的装饰器

from functools import wraps
def coroutine(func):
    @wraps(func)#把func相关属性复制过来
    def manage(*args,**kwargs):
        gen=func(*args,**kwargs)#获取生成器对象
        next(gen)#预激协程
        return gen#返回协程
    return manage

只需将@coroutine语法糖加在生成器函数上,就可以通过构造生成器对象获取活跃的协程。
注意:

  • 使用yield from调用协程时会自动预激,因此与@coroutine装饰器不兼容;
  • Python3.4标准库中的asyncio.coroutine装饰器不会预激协程,因此能兼容yield from句法。

2、终止协程

协程中未处理的异常会向上冒泡,传给next函数或send方法的调用方。
因此,终止协程的本质在于向协程发送其无法处理的异常。下面介绍三种方法终止协程:

  • 发送哨符值。常用None和Ellipsis,甚至StopIteration类也可以发送。
  • generator.throw(exc_type[,exc_value[,traceback]])
    令生成器在暂停的yield表达式处抛出指定的异常。若生成器处理了此异常,则生成器向前执行到下一个yield表达式,而产出的值会成为调用generator.throw得到的返回值。否则,异常会向上冒泡,传到调用方的上下文中。
  • generator.close()
    令生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器不处理此异常,或者跑出来StopIteration,调用方不会报错。如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。

后两种方法是自Python2.5开始显式发送异常的两个方法,建议使用后两种方法来终止协程。

3、处理异常

在使用协程的过程中会产生一些需要处理的异常,此时可利用try/except处理。如果不管协程如何结束都要做一些清理工作,请使用try/finally处理。

实例1:使用try/finally在协程终止时执行操作

class DemoException(Exception):
"""为这次演示定义的异常类型。 """
    def demo_finally():
        print('-> coroutine started')
        try:
            while True:
                try:
                   x = yield
                except DemoException:
                   print('*** DemoException handled. Continuing...')
                else:
                   print('-> coroutine received: {!r}'.format(x))
        finally:
            print('-> coroutine ending')

三、yield from结构

1、yield from结构介绍

作用介绍:
本质作用是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样两者可以直接发送和产出值,还可以直接传入异常。
替代产出值的嵌套for循环。

执行机制:
(1)在生成器gen中使用yield from subgen()时,subgen会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。与此同时,gen会阻塞,等待subgen终止。
(2)yield from结构会在内部自动捕获StopIteration异常,还会把对应的value属性值变成yield from表达式的值。

2、yield from的应用

实例1:对yield from架构双向通道本质的深入理解
下面我们结合实例深入理解yield from结构。假设我们需要利用yield from分别计算一个班级男女生身高和体重的平均值,并予以输出。采用“外部调用方+委派生成器+子生成器”的结构进行设计,结构示意图如下:
图片描述
实例代码:

from collections import namedtuple
Result=namedtuple('Result','average number')

def subaverager():#子生成器经委派生成器处理外部数据,并将值返回给委派生成器。
    total = 0.0
    number = 0
    average = None
    while True:
        term = yield
        if term is None:#外部调用方控制子生成器终止的关键语句。
            break
        total += term
        number += 1
        average=total/number
    return Result(average,number)

def averager(results,key):#委托生成器架构双向通道。
    while True:#避免StopIteration。当得到子生成器的返回值时,程序会执行到下一个yield。
        results[key]=yield from subaverager()

def main(grouper):
    results={}
    for key,group in grouper.items():
        term = averager(results,key)#构建生成器对象。
        next(term)
        for value in group:
            term.send(value)
        term.send(None)#外部调用方控制子生成器终止的语句。
        print(results)
    result(results)

def result(results):#格式化输出协程返回的处理数据。
    for key,value in results.items():
        gender,unit=key.split(';')
        print('{} {} averaging {:.2f} {}.'.format(
            value.number,gender,value.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(data)

思路扩展:上例展示的结构中仅有一个委派生成器和一个子生成器。事实上,这种调用关系可以扩展到更多的委托生成器上。即把多个委派生成器连接到一起。一个委派生成器调用另一个子生成器,这个子生成器本身也是委派生成器。这种链式结构最终以一个只使用yield的简单生成器结束,或者任何的可迭代对象结束。

实例2:替代产出值的嵌套for循环

def gen():
for c in 'AB':
 yield c
 for i in range(1, 3):
 yield i
print(list(gen()))#输出['A', 'B', 1, 2]

可以简化成:

def gen():
yield from 'AB'
yield from range(1, 3)
print(list(gen()))#输出['A', 'B', 1, 2]

3、PEP380中总结的yield from的六点行为

  • 子生成器产出的值都直接传给委派生成器的调用方。
  • 使用send()方法发给委派生成器的值都直接传给子生成器。如果发送None会调用生成器的__next__()方法。如果发送的不是None,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
  • 生成器退出时,生成器中的return表达式会触发StopIteration(expr)异常抛出。
  • yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。
  • 传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw()方法。如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。StopIteration之外的异常会向上冒泡,传给委派生成器。
  • 如果把 GeneratorExit 异常传入委派生成器, 或者在委派生成器上调用 close() 方法, 那么在子生成器上调用 close() 方法, 如果它有的话。 如果调用 close() 方法导致异常抛出, 那么异常会向上冒泡, 传给委派生成器; 否则, 委派生成器抛出GeneratorExit 异常。

四、协程与生成器异同

  • 生成器用于生成供迭代的数据。
  • 协程是数据的消费者,能完成协作式多任务活动。
  • 协程与迭代无关。尽管在协程中会使用 yield 产出值, 但这与迭代无关。

Hanwencheng
163 声望5 粉丝

Be yourself.