• 流畅的python——16 协程


    十六、协程

    生成器如何进化成协程

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

    协程演示

    In [51]: def s_c():
        ...:     print('c start')
        ...:     x = yield
        ...:     print('c received:',x)
        ...:
    
    In [52]: c = s_c()
    
    In [53]: c
    Out[53]: <generator object s_c at 0x00000221EF5DB5C8>
    
    In [54]: c.send(111)
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-54-ffa2730868ca> in <module>
    ----> 1 c.send(111)
    
    TypeError: can't send non-None value to a just-started generator
    
    In [55]: next(c)  # 首先调用 next ,因为生成器还没启动,没在 yield 暂停,不能发送数据。
    c start
    
    In [56]: next(c)  # yield 表达式 为 发送的值。协程会恢复,一直运行到下一个 yield 表达式,或者终止。
    c received: None
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-56-e846efec376d> in <module>
    ----> 1 next(c)
    
    StopIteration:
    
    In [57]: c.send(111)  # # yield 表达式 为 发送的值。
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-57-ffa2730868ca> in <module>
    ----> 1 c.send(111)
    
    StopIteration:
    

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

    'GEN_CREATED'

      等待开始执行。

    'GEN_RUNNING'

      解释器正在执行。

    ​ 只有在多线程应用中才能看到这个状态。此外,生成器对象在自己身上调用 getgeneratorstate 函数也行,不过这样做没什么用。

    'GEN_SUSPENDED'

      在 yield 表达式处暂停。

    'GEN_CLOSED'

      执行结束。

    In [59]: import inspect
    
    In [60]: inspect.getgeneratorstate(c)
    Out[60]: 'GEN_CLOSED'
    

    因为 send 方法的参数会成为暂停的 yield 表达式的值,所以,仅当协程处于暂停状态时才能调用 send 方法,例如 my_coro.send(42)。不过,如果协程还没激活(即,状态是'GEN_CREATED'),情况就不同了。因此,始终要调用 next(my_coro) 激活协程——也可以调用 my_coro.send(None),效果一样。

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

    In [62]: def cc(a):
        ...:     print('c start')
        ...:     b = yield a
        ...:     print('rsv:',b)
        ...:     c = yield a + b
        ...:     print('rsv:' ,c)
        ...:
    
    In [63]: c2 = cc(1)
    
    In [64]: inspect.getgeneratorstate(c2)
    Out[64]: 'GEN_CREATED'
    
    In [65]: c2.send(None)
    c start
    Out[65]: 1
    
    In [66]: inspect.getgeneratorstate(c2)
    Out[66]: 'GEN_SUSPENDED'
    
    In [67]: c2.send(2)
    rsv: 2
    Out[67]: 3
    
    In [68]: c2.send(3)
    rsv: 3
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-68-e3c5dc9e41ab> in <module>
    ----> 1 c2.send(3)
    
    StopIteration:
    
    In [69]: inspect.getgeneratorstate(c2)
    Out[69]: 'GEN_CLOSED'
    
    In [70]: c3 = cc(2)
    
    In [71]: next(c3)
    c start
    Out[71]: 2
    
    In [72]: dd = c3.send(3)
    rsv: 3
    
    In [73]: dd
    Out[73]: 5
    

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

    cc 协程的执行过程分为 3 个阶段:

    1 调用 next , 打印第一个消息,执行 yield a ,产出数字 a

    2 调用 send(11),把 11 赋值给 b,打印第二个消息,然后执行 yield a + b,产出数字 a + 11

    3 调用 send(12),把12 赋值给 c,打印第三个消息,协程终止。

    注意,各个阶段都在 yield 表达式中结束,而且下一个阶段都从那一行代码开始,然后再把 yield 表达式的值赋给变量

    使用协程计算移动平均值

    In [80]: def avg():
        ...:     total = 0
        ...:     c = 0
        ...:     avg = None
        ...:     while True:
        ...:         term = yield avg
        ...:         total+= term
        ...:         c += 1
        ...:         avg =total / c
        ...:
    
    # 这个无限循环表明,只要调用方不断把值发给这个协程,它就会一直接收值,然后生成结果。仅当调用方在协程上调用 .close() 方法,或者没有对协程的引用而被垃圾回收程序回收时,这个协程才会终止。
    # 使用协程的好处是,total 和 count 声明为局部变量即可,无需使用实例属性或闭包在多次调用之间保持上下文。
    
    In [81]: a = avg()
    
    In [82]: a.send(None)
    
    In [83]: a.send(1)
    Out[83]: 1.0
    
    In [84]: a.send(2)
    Out[84]: 1.5
    

    预激协程的装饰器

    函数调用产生一个生成器——》调用完成的返回的生成器可以一直生成值——》装饰器执行预激后,返回生成器。

    In [87]: def pre_next(gen):
        ...:     def inner(*args,**kwargs):  # 返回预激后的生成器
        ...:         g = gen(*args,**kwargs)
        ...:         next(g)
        ...:         return g
        ...:     return inner
        ...:
    
    In [88]: @pre_next
        ...: def avg():
        ...:     total = 0
        ...:     c = 0
        ...:     avg = None
        ...:     while True:
        ...:         term = yield avg
        ...:         total+= term
        ...:         c += 1
        ...:         avg =total / c
        ...:
    
    In [89]: a = avg()
    
    In [90]: a.send(1)
    Out[90]: 1.0
    
    In [91]: b = avg()
    
    In [92]: inspect.getgeneratorstate(b)
    Out[92]: 'GEN_SUSPENDED'
    

    使用 yield from 句法(参见 16.7 节)调用协程时,会自动预激,因此与示例 16-5 中的 @coroutine 等装饰器不兼容。

    终止协程和异常处理

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

    未处理的异常会导致协程终止
    In [93]: b.send(1)
    Out[93]: 1.0
    
    In [94]: b.send('a')
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-94-5d63b0dfa469> in <module>
    ----> 1 b.send('a')
    
    <ipython-input-88-028ea1232b5b> in avg()
          6     while True:
          7         term = yield avg
    ----> 8         total+= term
          9         c += 1
         10         avg =total / c
    
    TypeError: unsupported operand type(s) for +=: 'int' and 'str'
    
    In [95]: b.send(2)
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-95-19972eea9127> in <module>
    ----> 1 b.send(2)
    
    StopIteration:
    

    示例 16-7 暗示了终止协程的一种方式:发送某个哨符值,让协程退出。内置的 None 和 Ellipsis 等常量经常用作哨符值。Ellipsis 的优点是,数据流中不太常有这个值。我还见过有人把 StopIteration 类(类本身,而不是实例,也不抛出)作为哨符值;也就是说,是像这样使用的:my_coro.send(StopIteration)。

    从 Python 2.5 开始,客户代码可以在生成器对象上调用两个方法,显式地把异常发给协程。

    这两个方法是 throw 和 close。

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

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

    generator.close()

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

    In [1]: class DE(Exception):
       ...:     """自定义异常"""
       ...:
    
    # 的最后一行代码不会执行,因为只有未处理的异常才会中止那个无限循环,而一旦出现未处理的异常,协程会立即终止。
    In [2]: def demo_exc():
       ...:     print('c start')
       ...:     while True:
       ...:         try:
       ...:             x = yield
       ...:         except DE:
       ...:             print('DE raise')
       ...:         else:
       ...:             print('c received:{}'.format(x))
       ...:     raise RuntimeError('never run')
       ...:
    
    In [3]: e = demo_exc()
    
    In [4]: next(e)
    c start
    
    In [5]: next(e)
    c received:None
    
    In [6]: e.send(111)
    c received:111
    
    In [7]: e.close()  # 关闭生成器
    
    In [8]: from inspect import getgeneratorstate
    
    In [9]: getgeneratorstate(e)
    Out[9]: 'GEN_CLOSED'
    
    In [10]: e1 = demo_exc()
    
    In [11]: getgeneratorstate(e1)
    Out[11]: 'GEN_CREATED'
    
    In [12]: next(e1)
    c start
    
    # 如果把 DemoException 异常传入 demo_exc_handling 协程,它会处理,然后继续运行
    In [13]: e1.throw(DE)
    DE raise
    
    In [14]: getgeneratorstate(e1)
    Out[14]: 'GEN_SUSPENDED'
    
    In [15]: e1.send(22)
    c received:22
    
    # 但是,如果传入协程的异常没有处理,协程会停止,即状态变成 'GEN_CLOSED'。
    In [16]: e1.throw(ZeroDivisionError)
    ---------------------------------------------------------------------------
    ZeroDivisionError                         Traceback (most recent call last)
    <ipython-input-16-175ec079c766> in <module>
    ----> 1 e1.throw(ZeroDivisionError)
    
    <ipython-input-2-981b1ab8dc67> in demo_exc()
          3     while True:
          4         try:
    ----> 5             x = yield
          6         except DE:
          7             print('DE raise')
    
    ZeroDivisionError:
    
    In [17]: getgeneratorstate(e1)
    Out[17]: 'GEN_CLOSED'
    
    # 如果不管协程如何结束都想做些清理工作,要把协程定义体中相关的代码放入 try/finally 块中
    In [20]: def demo_exc():
        ...:     try:
        ...:         print('c start')
        ...:         while True:
        ...:             try:
        ...:                 x = yield
        ...:             except DE:
        ...:                 print('DE raise')
        ...:             else:
        ...:                 print('c received:{}'.format(x))
        ...:     finally:
        ...:         print('c end')
    

    让协程返回值

    In [21]: from collections import namedtuple
    
    In [22]: Res = namedtuple('Res','c avg')
    
    In [24]: def averager():
        ...:     t = 0
        ...:     c = 0
        ...:     while True:
        ...:         term = yield
        ...:         if term is None:
        ...:             break
        ...:         t += term
        ...:         c += 1
        ...:         avg = t/c
        ...:     return Res(c,avg)
    
    In [25]: a = averager()
    
    In [26]: next(a)
    
    In [27]: a.send(1)
    
    In [28]: a.send(2)
    
    # 一如既往,生成器对象会抛出 StopIteration 异常。异常对象的 value 属性保存着返回的值。
    In [29]: next(a)
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-29-15841f3f11d4> in <module>
    ----> 1 next(a)
    
    StopIteration: Res(c=2, avg=1.5)
    
    
    
    In [54]: a = averager()
    
    In [55]: a.send(None)
    
    In [56]: a.send(1)
    
    In [57]: a.send(2)
    
    # 捕获 StopIteration 异常,获取 averager 返回的值
    In [59]: try:
        ...:     a.send(None)
        ...: except StopIteration as exc:
        ...:     res = exc.value
        ...:
    
    In [60]: res
    Out[60]: Res(c=2, avg=1.5)
    

    获取协程的返回值虽然要绕个圈子,但这是 PEP 380 定义的方式,当我们意识到这一点之后就说得通了:yield from 结构会在内部自动捕获 StopIteration 异常。这种处理方式与 for 循环处理 StopIteration 异常的方式一样:循环机制使用用户易于理解的方式处理异常。对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把value 属性的值变成 yield from 表达式的值。可惜,我们无法在控制台中使用交互的方式测试这种行为,因为在函数外部使用 yield from(以及 yield)会导致句法出错。4

    4 iPython 有个扩展——ipython-yf(https://github.com/tecki/ipython-yf),安装这个扩展后可以在 iPython 控制台中直接执行 yield from。这个扩展用于测试异步代码,可以结合 asyncio 模块使用。这个扩展已经提交为 Python 3.5 的补丁,但是没有被接受。参见 Python 缺陷追踪系统中的 22412 号工单: Towards an asyncio-enabled command line(http://bugs.python.org/issue22412)。

    使用 yield from

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

    In [61]: def g():
        ...:     for c in 'AB':
        ...:         yield c
        ...:     for i in range(3):
        ...:         yield i
        ...:
    
    In [65]: list(g())
    Out[65]: ['A', 'B', 0, 1, 2]
    
    # 可以改写为:
    
    In [66]: def gen():
        ...:     yield from 'AB'
        ...:     yield from range(3)
        ...:
    
    In [67]: list(gen())
    Out[67]: ['A', 'B', 0, 1, 2]
    
    # itertools 模块 提供了优化版的 chain 函数,使用 C 语言编写,如下是相同功能的简单实现
    In [68]: def ch(*iterables):
        ...:     for i in iterables:
        ...:         yield from i
        ...:
    
    In [69]: list(ch('ABC',range(3)))
    Out[69]: ['A', 'B', 'C', 0, 1, 2]
    

    yield from x 表达式对 x 做的事:

    1 iter(x) 所以 x 可以是任何可迭代对象

    yield from 结构的本质作用不仅仅是替代产出值的嵌套 for 循环

    Syntax for Delegating to a Subgenerator

    yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。

    委派生成器

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

    子生成器

    yield from 表达式中 <iterable> 部分获取的生成器。

    调用方

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

    子生成器可能是简单的迭代器,只实现了 __next__ 方法;但是,yield from 也能处理这种子生成器。不过,引入 yield from 结构的目的是为了支持实现了 __next__sendclosethrow 方法的生成器。

        def averager():
            total = 0
            count = 0
            while True:
                x = yield
                if x is None:
                    break
                total += x
                count += 1
                return Res(count, total/count)
    
        a = averager()
        next(a)
        b = a.send(111)  # b 是 None ,没有得到返回值
        """生成器结束了,报错:StopIteration ,返回值:存放在异常的 value 中"""
        """
        Traceback (most recent call last):
        File ".\g.py", line 52, in <module>
            b = a.send(111)
        StopIteration: Res(count=1, average=111.0)
        """
        print(b)
    
    from collections import namedtuple
    
    Res = namedtuple('Res','count average')
    
    def averager():
        total = 0
        count = 0
        while True:
            x = yield
            if x is None:
                break
            total += x
            count += 1
            return Res(count, total/count)
    
    def report(res):
        print(res)  # {'girls;kg': None, 'girls;m': None, 'boys;kg': None, 'boys;m': None}
        for k,v in sorted(res.items()):
            group, unit = k.split(';')
            print('{:2} {:5} averaging {:.2f}{}'.format(
                v.count, group , v.average, unit
            ))
    

    正确的 averager :

    from collections import namedtuple
    
    Res = namedtuple('Res','count average')
    
    # 子生成器
    def averager():
        total = 0
        count = 0
        while True:
            x = yield
            if x is None:
                break
            total += x
            count += 1
        # 返回的 Result 会成为 grouper 函数中 yield from 表达式的值。
        # yield from 会处理 StopIterator 异常,返回结束的值
        return Res(count, total/count)  
    
    # 委派生成器
    def grouper(res, key):
        # 这个循环每次迭代时会新建一个 averager 实例;每个实例都是作为协程使用的生成器对象。
        while True:
            res[key] = yield from averager()
    
    # 客户端代码,即调用方
    def main(data):
        res = {}
        for k,v in data.items():
            group = grouper(res, k)
            next(group)
            for i in v:
                # 把各个 value 传给 grouper。传入的值最终到达 averager 函数中 term = yield 那一行;grouper 永远不知道传入的值是什么。
                group.send(i)
            # 把 None 传入 grouper,导致当前的 averager 实例终止,也让 grouper 继续运行,再创建一个 averager 实例,处理下一组值。
            group.send(None)
        report(res)
    
    # 输出报告
    def report(res):
        print(res)
        for k,v in sorted(res.items()):
            group, unit = k.split(';')
            print('{:2} {:5} averaging {:.2f}{}'.format(
                v.count, group , v.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)
    
    
    
    """
    (el_app) PS C:\Users\WangLin\Desktop\version8> python .\g.py
    {'girls;kg': Res(count=10, average=42.040000000000006), 'girls;m': Res(count=10, average=1.4279999999999997), 'boys;kg': Res(count=9, average=40.422222222222224), 'boys;m': Res(count=9, average=1.3888888888888888)}
     9 boys  averaging 40.42kg
     9 boys  averaging 1.39m
    10 girls averaging 42.04kg
    10 girls averaging 1.43m
    """
    
    In [76]: def a():
        ...:     yield 1
        ...:     return 1
        ...:
    
    In [80]: aa = a()
    
    In [81]: getgeneratorstate(aa)
    Out[81]: 'GEN_CREATED'
    
    In [82]: next(aa)
    Out[82]: 1
    
    In [83]: getgeneratorstate(aa)
    Out[83]: 'GEN_SUSPENDED'
    
    In [84]: b = aa.send(222)
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-84-b5a7980ee106> in <module>
    ----> 1 b = aa.send(222)
    
    StopIteration: 1
    
    In [85]: getgeneratorstate(aa)
    Out[85]: 'GEN_CLOSED'
    
    In [86]: b
    
    In [87]: c = aa.send(33)
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-87-d2f9f10a5b00> in <module>
    ----> 1 c = aa.send(33)
    
    StopIteration:
    
    In [88]: c
    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    <ipython-input-88-2b66fd261ee5> in <module>
    ----> 1 c
    
    NameError: name 'c' is not defined
    
    In [104]: c = b()
    
    In [105]: c.send(111)
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-105-ffa2730868ca> in <module>
    ----> 1 c.send(111)
    
    TypeError: can't send non-None value to a just-started generator
    
    In [97]: def b():
        ...:     while 1:
        ...:         yield from a()
        ...:
    
    In [98]: c = b()
    
    In [99]: next(c)
    Out[99]: 1
    
    In [100]: c.send(1)
    Out[100]: 1
    
    In [101]: c.send(1)
    Out[101]: 1
    
    In [102]: c.send(1)
    Out[102]: 1
    
    In [103]: c.send(1)
    Out[103]: 1
    
    why???
    
    In [127]: def a():
         ...:     print('yield do')
         ...:     yield 1  # 可迭代对象的值的组成
         ...:     print('return do')
         ...:     return 2  # 返回函数执行结果
    
    # 此时,a 只是一个函数,因为它显然要执行 return 
    # yield from 要先把 a() 变为可迭代对象,即 [1]
    # 所以,委派生成器 不断的循环 执行 a,即 取出 [1] 中的 1,这个 1 是 yield 的 1
    In [128]: list(a())
    yield do
    return do
    Out[128]: [1]
    
    In [129]: b = list(a())
    yield do
    return do
    
    In [130]: b
    Out[130]: [1]
    
    In [106]: def a():
         ...:     print('yield do')
         ...:     yield 1
         ...:     print('return do')
         ...:     return 2
    
    In [118]: d = a()
    
    In [119]: next(a)
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-119-15841f3f11d4> in <module>
    ----> 1 next(a)
    
    TypeError: 'function' object is not an iterator
    
    In [120]: def a():
         ...:     print('yield do')
         ...:     yield 1
         ...:     print('return do')
         ...:     #return 2
    
    In [121]: d = a()
    
    In [122]: next(d)
    yield do
    Out[122]: 1
    
    In [123]: d.send(2)
    return do
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-123-aca2617ea1f4> in <module>
    ----> 1 d.send(2)
    
    StopIteration:
    
    In [124]: def a():
         ...:     print('yield do')
         ...:     x = yield 1
         ...:     print('return do')
         ...:     if x is None:
         ...:         return 2
    
    In [125]: d = a()
    
    In [126]: next(d)
    yield do
    Out[126]: 1
    

    外层 for 循环每次迭代会新建一个 grouper 实例,赋值给 group 变量;group 是委派生成器。调用 next(group),预激委派生成器 grouper,此时进入 while True 循环,调用子生成器 averager 后,在 yield from 表达式处暂停。

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

    内层循环结束后,group 实例依旧在 yield from 表达式处暂停,因此,grouper函数定义体中为 results[key] 赋值的语句还没有执行。如果外层 for 循环的末尾没有 group.send(None),那么 averager 子生成器永远不会终止,委派生成器 group 永远不会再次激活,因此永远不会为 results[key]赋值。

    外层 for 循环重新迭代时会新建一个 grouper 实例,然后绑定到 group 变量上。前一个 grouper 实例(以及它创建的尚未终止的 averager 子生成器实例)被垃圾回收程序回收。

     这个试验想表明的关键一点是,如果子生成器不终止,委派生成器会在 yield from 表达式处永远暂停。如果是这样,程序不会向前执行,因为 yield from(与 yield 一样)把控制权转交给客户代码(即,委派生成器的调用方)了。显然,肯定有任务无法完成。

    因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用 yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器,以此类推。最终,这个链条要以一个只使用 yield表达式的简单生成器结束;不过,也能以任何可迭代的对象结束

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

    yield from 的意义

    “把迭代器当作生成器使用,相当于把子生成器的定义体内联在 yield from 表达式中。此外,子生成器可以执行 return 语句,返回一个值,而返回的值会成为 yield from 表达式的值。”

    PEP 380 中已经没有这段宽慰人心的话,因为没有涵盖所有极端情况。不过,一开始可以这样粗略地说。

    批准后的 PEP 380 在“Proposal”一节(https://www.python.org/dev/peps/pep-0380/#proposal)分六点说明了 yield from 的行为。这里,我几乎原封不动地引述,不过把有歧义的“迭代器”一词都换成了“子生成器”,还做了进一步说明。示例 16-17 阐明了下述四点。

    1 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。

    2 使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的__next__() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。

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

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

    yield from 结构的另外两个特性与异常和终止有关。

    1 传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的 throw() 方法。如果调用 throw() 方法时抛出 StopIteration 异常,委派生成器恢复运行。StopIteration 之外的异常会向上冒泡,传给委派生成器。

    2 如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么在子生成器上调用 close() 方法,如果它有的话。如果调用 close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常。

    RESULT = yield from EXPR
    
    _i = iter(EXPR)
    try:
        _y = next(_i)
    except StopIteration as _e:
         _r = _e.value
    else:
         while 1:
            _s = yield _y
         try:
            _y = _i.send(_s)
         except StopIteration as _e:
            _r = _e.value
            break
    RESULT = _r
    
    '''
    _i(迭代器)
      子生成器
    _y(产出的值)
      子生成器产出的值
    _r(结果)
      最终的结果(即子生成器运行结束后 yield from 表达式的值)
    _s(发送的值)
      调用方发给委派生成器的值,这个值会转发给子生成器
    _e(异常)
      异常对象(在这段简化的伪代码中始终是 StopIteration 实例)
    '''
    
    _i = iter(EXPR) ➊
    try:
        _y = next(_i) ➋
    except StopIteration as _e:
        _r = _e.value ➌
    else:
        while 1: ➍
            try:
                _s = yield _y ➎
            except GeneratorExit as _e: ➏
                try:
                     _m = _i.close
                 except AttributeError:
                     pass
                 else:
                    _m()
                     raise _e
             except BaseException as _e: ➐
                 _x = sys.exc_info()
                 try:
                     _m = _i.throw
                 except AttributeError:
                     raise _e
                 else: ➑
                     try:
                         _y = _m(*_x)
                     except StopIteration as _e:
                         _r = _e.value
                         break
             else: ➒
                 try: ➓
                     if _s is None: ⓫
                         _y = next(_i)
                     else:
                         _y = _i.send(_s)
                 except StopIteration as _e: ⓬
                     _r = _e.value
                     break
    RESULT = _r ⓭
    

    使用案例 : 使用协程做离散事件仿真

    离散事件仿真是一种把系统建模成一系列事件的仿真类型。

    In [1]: from collections import namedtuple
    
    In [2]: E = namedtuple('E','time proc action')
    
    In [3]: def taxi_process(i,n,start_time):  # 出租车编号,载客次数,出车时间
       ...:     time = yield E(start_time, i, 'leave garage')  # 出租车出车
       ...:     for k in range(n):
       ...:         time = yield E(time,i,'pick up passenger')  # 乘客上车
       ...:         time = yield E(time,i,'drop off passenger')  # 乘客下车
       ...:     yield E(time,i,'going home')
       ...:
    
    In [4]: t = taxi_process('xmen',3,8)
    
    In [5]: next(t)
    Out[5]: E(time=8, proc='xmen', action='leave garage')
    
    In [6]: _
    Out[6]: E(time=8, proc='xmen', action='leave garage')
    
    In [7]: t.send(_.time+10)
    Out[7]: E(time=18, proc='xmen', action='pick up passenger')
    
    In [8]: t.send(_.time+1)
    Out[8]: E(time=19, proc='xmen', action='drop off passenger')
    
    In [9]: t.send(_.time+10)
    Out[9]: E(time=29, proc='xmen', action='pick up passenger')
    
    In [10]: t.send(_.time+1)
    Out[10]: E(time=30, proc='xmen', action='drop off passenger')
    
    In [11]: t.send(_.time+10)
    Out[11]: E(time=40, proc='xmen', action='pick up passenger')
    
    In [12]: t.send(_.time+1)
    Out[12]: E(time=41, proc='xmen', action='drop off passenger')
    
    In [13]: t.send(_.time+10)
    Out[13]: E(time=51, proc='xmen', action='going home')
    
    In [14]: t.send(_.time+1)
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-14-f9e5703fbef3> in <module>
    ----> 1 t.send(_.time+1)
    
    StopIteration:
    

    在这个仿真系统中,各个出租车协程由 Simulator.run 方法中的主循环驱动。仿真“钟”保存在 sim_time 变量中,每次产出事件时都会更新仿真钟。

    构造 taxis 字典

    taxis = {i: taxi_process(i, (i + 1) * 2, i * DEPARTURE_INTERVAL)
                 for i in range(num_taxis)}
    sim = Simulator(taxis)
    

    构造结果

    taxis = {0: taxi_process(ident=0, trips=2, start_time=0),
             1: taxi_process(ident=1, trips=4, start_time=5),
             2: taxi_process(ident=2, trips=6, start_time=10)}
    sim = Simulator(taxis)
    

    main 函数

    sim = Simulator(taxis)
    sim.run(end_time)
    

    出租车仿真类

    class Simulator:
        def __init__(self, procs_map):
            self.events = queue.PriorityQueue()
            self.procs = dict(procs_map)
        def run(self, end_time):
            # 排定各辆出租车的第一个事件
            for _, proc in sorted(self.procs.items()):
                first_event = next(proc)
                self.events.put(first_event)
            # 这个仿真系统的主循环
            sim_time = 0
            while sim_time < end_time:
                if self.events.empty():
                    print('*** end of events ***')
                    break
                current_event = self.events.get()
                sim_time, proc_id, previous_action = current_event
                print('taxi:', proc_id, proc_id * ' ', current_event)
                active_proc = self.procs[proc_id]
                next_time = sim_time + compute_duration(previous_action)
                try:
                    next_event = active_proc.send(next_time)
                except StopIteration:
                    del self.procs[proc_id]
                else:
                    self.events.put(next_event)
            else:
                msg = '*** end of simulation time: {} events pending ***'
                print(msg.format(self.events.qsize()))
    

    这个示例的要旨是说明如何在一个主循环中处理事件,以及如何通过发送数据驱动协程。这是 asyncio 包底层的基本思想。

  • 相关阅读:
    cookie
    sql 语句
    页面宽高
    分页
    asp.net中如何防止用户重复点击提交按钮
    小试简单工厂模式之简单计算器
    用函数实现交换的疑问
    结构体变量输入输出的问题
    scanf函数输入float数需要注意的问题
    oracle学习手记(1)
  • 原文地址:https://www.cnblogs.com/pythonwl/p/15793642.html
Copyright © 2020-2023  润新知