流畅的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 包底层的基本思想。

posted @ 2022-01-12 17:44  pythoner_wl  阅读(102)  评论(0)    收藏  举报