协程

 

yield 含义

对Python生成器中yield来说,有两层含义:产出和让步

yield item会产出一个值,提供给next(...)的调用方;还会做出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next(...)。

 

从句法上看,协程和生成器类似,都是定义体中包含yield关键字的函数。可是,在协程中yield通常出现在表达式的右边(data = yield),可以产出值,也可以不产出。

如果yield关键字后面没有表达式,那么生成器产出None。协程可以从调用方接受数据,通过使用.send(data)方法。

yield关键字甚至还可以不接收或传出数据。不管数据如何流动,yield都是一种流程控制工具,使用它可以实现协作式多任务;

协程可以把控制器让步给中心调度程序,从而激活其他的协程。

 

从根本上把yield视作控制流程的方式,这样就好理解协程了。

 

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

>>> def simple_coroutine():
...     print('-> coroutine started')
...     x = yield
...     print('-> coroutine received:',x)
...
>>> my_coro = simple_coroutine()
>>> my_coro
<generator object simple_coroutine at 0x000001881C76DE08>
>>> next(my_coro)
-> coroutine started
>>> my_coro.send(998)
-> coroutine received: 998
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

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

(1)‘GEN_CREATED’,等待开始执行

(2)‘GEN_RUNNING’,解释器正在执行

(3)‘GEN_SUSPENDED’,在yield表达式处暂停

(4)‘GEN_CLOSED’,执行结束

▲ 仅当协程处于暂停状态时才能调用send(...)方法,因此始终要调用next(my_coro)激活协程,也可以调用my_coro.send(None),效果一样。

最先调用next(...)函数这一步通常称为‘预激’(prime)协程。让协程向前执行到第一个yield表达式,准备好作为活跃的协程使用。

 

>>> def simple_coro2(a):
...     print('-> Started: a =',a)
...     b = yield a
...     print('-> Received: b =',b)
...     c = yield a + b
...     print('-> Received: c =',c)
...
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2)
'GEN_CREATED'
>>> next(my_coro2)
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2)
'GEN_SUSPENDED'
>>> my_coro2.send(28)
-> Received: b = 28
42
>>> my_coro2.send(99)
-> Received: c = 99
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

 

协程在yield关键字所在的位置暂停执行。在赋值语句中,=右边的代码在赋值之前执行。

因此,对于b=yield a 这行代码来说,等到客户端代码再次激活协程时,才会设定b的值。

运行流程:

(1)调用next(my_coro2),打印第一个消息,然后执行yield a,产出数字14。

(2)调用my_coro2.send(28),打印第二个消息,然后执行yield a+b,产出数字42.

(3)调用my_coro2.send(99),打印第三个消息,协程终止。

 

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

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

>>> coro_avg = simple_averager()
>>> next(coro_avg)
>>> coro_avg.send(10)
10.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(5)
15.0

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

 

三、预激协程的装饰器

  使用协程之前必须预激,可以在协程上使用一个特殊的装饰器。

from functools import wraps

def coroutine(func):

    @wraps(func)
    def prime(*args,**kwargs):
        gen = func(*args,**kwargs)
        next(gen)
        return gen
    return prime

@coroutine
def simple_averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term  = yield average
        count += 1 
        total += term
        average = total/count

>>> coro_avg = simple_averager()
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(coro_avg)
'GEN_SUSPENDED'

 

▲ 使用yield from句法调用协程时,会自动预激,因此与@coroutine等装饰器不兼容。

▲ 使用asyncio.coroutine装饰器不会预激协程,因此能兼容yield from句法。

 

四、终止协程和异常处理

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

  终止协程的一种方式:发送某个哨符值,让协程退出。内置的None和Ellipsis等常量经常用作哨符值。也可以使用StopIteration类。如(my_coro.send(StopIteration))

  Python2.5开始新增两个方法:

    generator.throw(exc_type[, exc_value[, traceback]),使生成器在暂停的yield表达式处抛出指定的异常。

      如果生成器处理了抛出的异常,代码会向前直行道下一个yield表达式,而产出的值会称为调用generator.throw方法得到的返回值。

    generator.close(),使生成器在暂停的yield表达式处抛出GeneratorExit异常。

 

class DemoException(Exception):
    ''''''

def demo_exc_handling():
    print('-> coroutine started')
    while True:
        try:
            x = yield
        except DemoException:
            print('*** DemoException handled. Continuing....')
        else:
            print('-> coroutine received: {!r}'.format(x))
    raise RuntimeError('This line should never run...')

 

▲ 如果传入协程的异常没有处理,协程会停止,状态变成'GEN_CLOSED'。

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

 

五、让协程返回值

  为了返回值,协程必须正常终止。

  例子中加入了判断条件,以便退出循环。

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

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

 

发送None会终止循环,导致协程结束,生成器对象抛出StopIteration异常,返回return的结果值。

▲ return表达式的值会传给调用方,赋值给StopIteration异常的一个属性。

try:
    coro_avg.send(None)
except StopIteration as exc:
    result = exc.value

 

获取协程的返回值虽然要绕个圈,但这是PEP380定义的方法。

yield from结构 会在内部自动捕获StopIteration异常。与for循环处理StopIteration异常的方式一样。

 

六、使用 yield from

  yield from是全新的语言结构,作用比yield多很多,在其他语言中,类似的结构使用await关键字。

  在生成器gen中使用yield from subgen()时,subgen会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。于此同时,gen会阻塞,等待subgen终止。

  yield from主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来。

  这样二者可以直接发送和产出值,还可以直接传入异常,而不用在中间的协程中添加大量处理异常的样板代码。

 

  委派生成器:包含 yield from <iterable>表达式的生成器函数

  子生成器:从yield from <iterable>部分获取的生成器。

  调用方:指调用委派生成器的客户端代码。

 

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

def averager():   #子生成器
    total = 0.0
    count = 0
    average = None
    while True:
        term  = yield
        if term is None:
            break
        count += 1
        total += term
        average = total/count
    return Result(count,average)

def grouper(result, key):    #委派生成器
    while True:
        result[key] = yield from averager()

def main(data):    #调用方

    result = {}
    for key,values in data.items():
        group = grouper(result,key)
        next(group)
        for value in values:
            group.send(value)
        group.send(None)
        report(result)

def report(Result):
    for key,values in Result.items():
        print(key,values)

data = {
    'girls;kg':
        [40.9,23,56,78,48,52,38.0],
}

main(data)

 

运行方式:

(1)外层for循环每次迭代会新建一个grouper实例,复制给group变量,group是委派生成器。

(2)调用next(group),预激委派生成器,此时进入while True循环,调用子生成器averager后,在yield from表达式处暂停。

(3)内层for循环调用group.send(value),直接把值传给子生成器averager。同时,当前的grouper实例(group)在yield from表达式处暂停。

(4)内层循环结束后,group实例依旧在yield from处暂停,因此,grouper函数定义体中result[key]赋值语句还没有执行。

(5)如果外层for循环的末尾没有group.send(None),那么averager子生成器永远不会终止,委派生成器group永远不会再次激活,因此永远不会为result[key]赋值。

(6)外层for循环重新迭代时会新建一个grouper实例,然后绑定到group变量上。前一个grouper实例被垃圾回收程序回收。

 

▲ 委派生成器相当于管道。

▲ 可以把任意数量的委派生成器连接在一起。而子生成器本身也是一个委派生成器,使用yield from调用另一个子生成器,以此类推。

▲ 最终这个链条要以一个只使用表达式yield的简单生成器结束,也可以任何可迭代的对象结束。

 

七、yield from的意义

 

子生成器产出的值都直接传给委派生成器的调用方。

使用send()方法发给委派生成器的值都直接传给子生成器。

生成器退出时,生成器(或子生成器)中的return expr表达式会触发StopIteration(expr)异常抛出。

yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。

 

八、使用协程做离散事件仿真

  在离散事件仿真中,仿真‘钟’向前推荐的量不是固定的,而是直接推进到下一个事件模型的模拟时间。

 

posted @ 2019-08-31 12:37  5_FireFly  阅读(200)  评论(0编辑  收藏  举报
web
counter