• 协程


    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异常的第一个参数。

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

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

  • 相关阅读:
    转:关于JAVA多线程同步
    转:Java HashMap实现详解
    索引创建规则:
    数据库为什么要分库分表
    [设计模式] javascript 之 桥接模式
    [百度地图] ZMap 与 MultiZMap 封装类说明;
    [设计模式] Javascript 之 外观模式
    [设计模式] javascript 之 代理模式
    [设计模式] javascript 之 装饰者模式
    [设计模式] javascript 之 适配器模式
  • 原文地址:https://www.cnblogs.com/5poi/p/11438588.html
Copyright © 2020-2023  润新知