协程的案例解析

    协程可以看做加强版生成器,这从协程定义的文档名称就可以看出,协程的底层架构在“PEP 342—Coroutines via Enhanced Generators”中定义。

    定义在Python 2.5(2006 年)实现了。自此之后,yield 关键字可以在表达式中使用,而且生成器 API 中增加了 .send(value) 方法。生成器的调用方可以使用 .send(...) 方法发送数据,发送的数据会成为生成器函数中 yield 表达式的值。因此,生成器可以作为协程使用。

    自Python 3.3(2012 年)实现的“PEP 380—Syntax for Delegating to a Subgenerator”对生成器函数的句法做了两处改动,以便更好地作为协程使用:

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

以下所有案例代码均在《流畅的python》第16章的案例基础上做了修改。

案例1:

def simple_coroutine():
    print('coroutine started')
    x = yield
    print('coroutine received:',x)

if __name__=='__main__':
    gen = simple_coroutine()
    print(gen)
    next(gen)
    gen.send(42)

代码运行结果:

<generator object simple_coroutine at 0x000000F033696AF0>
coroutine started
coroutine received: 42
Traceback (most recent call last):
...
StopIteration
案例分析:

    第一个案例比较直观的演示了协程的用法。通过代码可以看到此时yield 在表达式中使用;如果协程只需从客户那里接收数据,那么产出的值是 None——这个值是隐式指定的,因为 yield 关键字右边没有表达式。

与创建生成器的方式一样,调用函数得到生成器对象。打印变量gen可以验证此时引用的确实是一个生成器对象。此时生成器还没启动,无法发送数据,需要先调用一遍 next(...) 函数,这称为协程的预激(prime,所谓预激就是让协程的代码执行到第一个 yield 表达式位置)。 此时代码会在yield所在表达式位置挂起。这里需要特别注意,一个赋值语句是分为三步的,即声明变量并开辟内存空间,计算赋值运算符右侧的表达式值,计算完毕后将值赋给赋值运算符左侧的变量。协程中代码是在赋值语句的最后一步挂起,也就是赋值运算符右侧的表达式已经计算完毕,就要进行给x赋值时代码挂起。此时,右侧表达式的计算结果并不是准备赋值给左侧的x的,而是返回到了调用者位置(也就是调用next(gen)的位置),而真正给x赋值的内容是接下来通过send方法传入的内容。本例中比较特殊之处在于,赋值语句右侧没有表达式,那么返回到next(gen)位置的值将是None。调用send(42)方法后,yield表达式利用传入的42为x赋值。现在协程会恢复,并一直运行到下一个 yield 表达式位置或者运行到结尾。当代码运行到协程定义体的末尾,导致生成器像往常一样抛出 StopIteration 异常。

对于协程如果没有经过预激就开始调用send方法生成值,会产生如下异常:

Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator

案例2:

def simple_coroutine(a):
    print('coroutine start, a: ',a)
    b = yield a
    print('receive b: ',b)
    c = yield a+b
    print('received c: ',c)
if __name__=='__main__':
    gen = simple_coroutine(14)
    print(next(gen))
    print(gen.send(28))
    gen.send(99)

运行结果:

coroutine start, a:  14
14
receive b:  28
42
received c:  99
Traceback (most recent call last):
...
StopIteration

案例分析:

调用next(gen)是完成预激,让代码运行到第一个yield位置挂起,根据案例一的分析,挂起是在赋值运算符右侧表达式运算完毕准备进行赋值操作时挂起,而且根据案例一的分析还知道,右侧表达式运算结果是返回到调用处(而不是用来给赋值运算符左侧的变量真正赋值的),此时右侧表达式计算结果为14(也就是参数a的值),返回到了调用next(gen)的地方被打印到了控制台。当调用send(28)的时候,协程代码复活,利用传入的28完成了赋值语句,并继续运行到下一个yield位置挂起。挂起时赋值运算符右侧的表达式a+b的计算结果42被返回到了send(28)调用处,42被打印到了控制台上。随后调用send(99),协程代码复活利用99完成了变量c的赋值,并继续运行到协程代码的终止处产生StopIteration异常。

案例3:

def myavg():
    total = 0.0
    count = 0
    result = None
    while True:
        temp = yield result
        total += temp
        count += 1
        result = total / count

if __name__=='__main__':
    gen = myavg()
    next(gen)
    print(gen.send(10))
    print(gen.send(20))
    print(gen.send(60))

运行结果:

10.0
15.0
30.0

案例分析:

调用next(gen),代码运行到第一个yield位置,此时右侧表达式的值None返回到了调用处,如果print(next(gen))就会看见一个None。send(10)让赋值语句完成,temp为10,协程代码继续运行到直到下一次yield处,挂起时将计算得到result返回到send(10)调用处,因此10.0被打印到了控制台。继续send(20),协程代码复活,temp赋值为20,result计算为15,在yield处挂起时将计算结果15返回到调用处被打印到控制台。继续send(60),协程代码复活,temp赋值为60,result计算为30,在yield处挂起时将计算结果30返回到调用处被打印到控制台。

通过以上3个案例可以看到协程使用前必须要经过预激,否则会产生错误。如何有效的避免忘记预激呢?另外,第3个案例中,while循环会一直处于工作状态,如何让协程终止呢?继续看下面的案例。

案例4

利用装饰器完成协程的预激。

def mywrap(func):
    def temp(*args,**kwargs):
        gen = func(*args,**kwargs)
        next(gen)
        return gen
    return temp

mywarp是一个装饰器函数,调用mywarp函数获得temp函数,向temp函数中传入func(也就是真正的协程函数)所需要的参数后获得预激好的生成器。

使用方式如下:

def mywrap(func):
    def temp(*args,**kwargs):
        gen = func(*args,**kwargs)
        next(gen)
        return gen
    return temp

def myavg():
    total = 0.0
    count = 0
    result = None
    while True:
        temp = yield result
        total += temp
        count += 1
        result = total / count

if __name__=='__main__':
    wrap = mywrap(myavg)
    gen = wrap()
    print(gen.send(10))
    print(gen.send(20))
    print(gen.send(60))

也可以使用装饰器的语法糖:

def mywrap(func):
    def temp(*args,**kwargs):
        gen = func(*args,**kwargs)
        next(gen)
        return gen
    return temp

@mywrap
def myavg():
    total = 0.0
    count = 0
    result = None
    while True:
        temp = yield result
        total += temp
        count += 1
        result = total / count

if __name__=='__main__':
    gen = myavg()
    print(gen.send(10))
    print(gen.send(20))
    print(gen.send(60))

使用后面介绍的yield from句法可以自动预激子协程,因此使用yield from与mywrap装饰器就会产生冲突。

案例5

利用throw()或close()方法产生指定的异常终止协程的运行。

throw(异常类型):

在暂停的yield表达式处抛出指定的异常。如果生成器利用try...except...处理了该异常,代码会执行到下一个yield处。如果生成器自身没有处理该异常,该异常会向上抛出到调用方。

close():

在暂停的yield表达式处抛出GeneratorExit异常。注意:如果生成器没有处理这个异常或抛出了StopIteration异常(也就是yield表达式从挂起处恢复后执行到了结束),调用方不会有报错!但如果收到了GeneratorExit异常后生成器还在产出值,则解释器抛出RuntimeError异常。

class DemoException(Exception):
    pass

def demo_exc_handling():
    print('-> coroutine started')
    while True:
        try:
            x = yield
        except DemoException:
            print('handle DemoException!')
        else:
            print('get ',x)
    raise Exception('???')

if __name__=='__main__':
    gen = demo_exc_handling()
    next(gen)
    gen.send(10)
    gen.throw(DemoException())
    gen.send(20)
    gen.close()

运行结果:

-> coroutine started
get  10
handle DemoException!
get  20

案例分析:

创建生成器后通过预激,协程代码运行到x=yield处挂起,此时控制台上打印->coroutine started,代码在x=yield处挂起,yield右侧的变量表达式返回None到调用处。执行gen.send(10)时,yield表达式恢复运行并将传入的10赋值给x,此时没有异常产生于是执行else中的代码,在控制台输出get 10,继续下一轮循环,返回None并在yield赋值时挂起。执行gen.throw(DemoException),此时yield恢复执行并抛出DemoException,异常会被except捕获,在控制台输出handle DemoException后,代码继续执行到yield表达式赋值处,返回None到调用处继续挂起。然后执行send(20)与send(10)过程类似,最后执行close(),在yield表达式挂起处抛出GeneratorExit异常,根据前面的描述,该异常未被捕获,所以调用方并没有接收到任何报错。且协程由挂起状态转为关闭状态。

协程一共有四种状态,分别是等待开始执行的GEN_CREATED,解释器正在执行状态GEN_RUNNING,在yield表达式处挂起状态GEN_SUSPEND,和执行结束状态GEN_CLOSED。查看协程的状态使用inspect.getgeneratorstate函数即可。

我们稍微修改下案例5,插入inspect.getgeneratorstate函数观察协程gen的状态变化:

if __name__=='__main__':
    gen = demo_exc_handling()
    print(inspect.getgeneratorstate(gen))
    next(gen)
    print(inspect.getgeneratorstate(gen))
    gen.send(10)
    gen.throw(DemoException())
    gen.send(20)
    gen.close()
    print(inspect.getgeneratorstate(gen))

运行结果:

GEN_CREATED
-> coroutine started
GEN_SUSPENDED
get  10
handle DemoException!
get  20
GEN_CLOSED

在创建gen对象之后但激活之前,gen的状态为GEN_CREATED,预激之后,协程在yield表达式赋值处挂起,因此gen的状态为GEN_SUSPENDED,最后在执行完close()后,协程关闭,状态为GEN_CLOSED。

如果传入了生成器无法处理的异常,异常会被抛出,且协程终止。

案例6 

让协程有返回值。

python3.3版本之前,在协程中出现return语句会产生错误。

修改myavg函数,让其有一个返回值:

Result = namedtuple('Result','count average')
def myavg():
    total = 0.0
    count = 0
    average = None
    while True:
        temp = yield
        if temp is None:
            break
        total += temp
        count += 1
        average = total / count
    return Result(count,average)

if __name__=='__main__':
    gen = myavg()
    next(gen)
    gen.send(10)
    gen.send(20)
    gen.send(30)
    gen.send(None)

注意协程的循环体有改变,当temp的值为None的时候,协程就会运行终止。随着协程运行终止就会抛出StopIteration异常。而生成器返回的结果Result对象将作为抛出的StopIteration对象的value属性值被提交到调用处。如果为了获取这个返回值,需要捕获抛出的StopIteration异常然后获取其value属性值:

if __name__=='__main__':
    gen = myavg()
    next(gen)
    gen.send(10)
    gen.send(20)
    gen.send(30)
    try:
        gen.send(None)
    except StopIteration as e:
        result = e.value
    print(result)

代码运行结果:

Result(count=3, average=20.0)

这样设计的目的主要是为了保证协程结束时抛出StopIteration的机制。

python的yield from可以像for循环那样在语句内部处理掉StopIteration异常并获得协程的返回值。看下面的例子。

案例7

使用yield from。

yield from与for类似也可以内部处理StopIteration异常,先看yield from替代for的用法:

def myfor():
    for ch in 'ABC':
        yield ch
    for i in range(1,3):
        yield i

def myyieldfrom():
    yield from 'ABC'
    yield from range(1,3)

if __name__=='__main__':
    print(list(myfor()))
    print(list(myyieldfrom()))

运行结果:

['A', 'B', 'C', 1, 2]
['A', 'B', 'C', 1, 2]

然后yield from的作用不仅于此,yield from的作用是可以获得一个委派生成器,但是这个委派生成器本身不处理传入的内容,而是像一个通道,将客户端利用派生生成器传入的值直接传递给子生成器,当子生成器处理完数据协程关闭,并随StopIteration将结果返回的时候,委派生成器会处理StopIteration获得value属性的值也就是子生成器的结果。看案例:

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],
}
Result = namedtuple('Result', 'count average')


# 子生成器
def averager():
    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 averager()


def main(data):
    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))

if __name__=='__main__':
    main(data)

运行结果:

 9 boys  averaging 40.42kg
 9 boys  averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m
案例分析:

data为某幼儿园10名女生和9名男生的生身高和体重信息。现在要求出他们的平均值。案例中的grouper函数带yield from语句,因此它是一个委派生成器,执行grouper()其返回值就是委派生成器。在main方法中,当执行完语句group = grouper(results,key)之后,就获得了委派生成器group,随即next(group)是在预激。随后在for value in values中,调用group.send(value)时,就是将值源源不断的提交给子生成器。谁是子生成器?grouper函数中,yield from后面跟的是myavg函数的返回值,myavg返回值是一个生成器,也就是此时委派生成器的子生成器。

具体的执行步骤就是,next(group)时,委派生成器挂起在yield from表达式赋值处。随后for value in values循环中,不断通过group.send出来的值都直接交给子生成器处理。整个传值过程中group一直处于挂起状态。当values中的数据传递完毕(例如10个女孩的体重数据),此时循环结束,马上执行的是group.send(None),子生成器收到None离开结束协程,抛出StopIteration异常并用value属性携带者子生成器的返回值。此时yield from恢复运行,处理StopIteration异常拿到value属性值并赋值给results[key]。然后继续运行到写一个yield from处。此时myavg函数再次运行生成了新的子生成器,group挂起。但是在main方法中,当group.send(None)完毕后会执行外层循环的下一个循环,下一个循环一开始就调用了grouper函数,这样就重新新建了一个委派生成器group,然后继续通过该group传递数据到子生成器。

因此每次处理新的一组数据时,都会新建一个委派生成器,委派生成器将该组组内的所有数据都交给子生成器处理,当该组数据处理完成后,派生生成器发送None,子生成器产生StopIteration异常并返回结果,yield from恢复运行,处理StopIteration异常,拿到结果赋值给results[key]。




猜你喜欢

转载自blog.csdn.net/piglite/article/details/79470783