1四、细说协程

【节选自《流畅的Python》第16章-协程】python

1、综述算法

字典为动词“to yield”给出了两个释义:产出和让步。对于 Python 生成器中的 yield 来讲,这两个含义都成立。编程

yield item 这行代码会产出一个值,提供给 next(...) 的调用方;数据结构

此外,还会做出让步,暂停执行生成器,让调用方继续工做,直到须要使用另外一个值时再调用next()。调用方会从生成器中拉取值。多线程

从句法上看,协程与生成器相似,都是定义体中包含 yield 关键字的函数。闭包

但是,在协程中,yield 一般出如今表达式的右边(例如,datum = yield),能够产出值,也能够不产出——并发

若是 yield关键字后面没有表达式,那么生成器产出 None。dom

协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是 next(...) 函数。一般,调用方会把值推送给协程。异步

yield 关键字甚至还能够不接收或传出数据。async

无论数据如何流动,yield 都是一种流程控制工具,使用它能够实现协做式多任务:协程能够把控制器让步给中心调度程序,从而激活其余的协程。

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

 

2、协程最简单的使用演示

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

my_coro = simple_coroutine()

print(my_coro)    #generator object

next(my_coro)

print(my_coro.send(42))

结果以下:

一、协程使用生成器函数定义:定义体中有 yield 关键字。

二、yield 在表达式中使用;若是协程只需从客户那里接收数据,那么产出的值是 None——这个值是隐式指定的,由于 yield 关键字右边没有表达式。

三、与建立生成器的方式同样,调用函数获得生成器对象。

四、首先要调用 next(...) 函数,由于生成器还没启动,没在 yield 语句处暂停,因此一开始没法发送数据。

五、调用这个方法后,协程定义体中的 yield 表达式会计算出 42;如今,协程会恢复,一直运行到下一个 yield 表达式,或者终止。

六、最后,控制权流动到协程定义体的末尾,致使生成器像往常同样抛出 StopIteration 异常。

 

3、协程能够身处四个状态

当前状态可使用inspect.getgeneratorstate(...) 函数肯定,该函数会返回下述字符串中的一个。

一、'GEN_CREATED'         等待开始执行。

2'GEN_RUNNING'         解释器正在执行。

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

三、'GEN_SUSPENDED' 在 yield 表达式处暂停。

4'GEN_CLOSED'           执行结束。

 

由于 send 方法的参数会成为暂停的 yield 表达式的值,因此,仅当协程处于暂停状态时才能调用 send 方法,例如 my_coro.send(42)。

不过,若是协程还没激活(即,状态是 'GEN_CREATED'),状况就不一样了。

所以,始终要调用 next(my_coro) 激活协程——也能够调用my_coro.send(None),效果同样

若是建立协程对象后当即把 None 以外的值发给它,会出现下述错误:

my_coro = simple_coroutine()
my_coro.send(23)

注意错误消息,它表述得至关清楚。最早调用 next(my_coro) 函数这一步一般称为“预激”(prime)协程。

(即,让协程向前执行到第一个 yield 表达式,准备好做为活跃的协程使用)

 

下面举个产出多个值的例子,以便更好地理解协程的行为:

from inspect import getgeneratorstate

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)

state = getgeneratorstate(my_coro2)
print(state)
# GEN_CREATED

next(my_coro2)
# -> Started: a= 14
state = getgeneratorstate(my_coro2)
print(state)
# GET_SUSPENDED

my_coro2.send(28)
# -> Received: b= 28
my_coro2.send(99)
# -> Received: c= 99
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# StopIteration
state = getgeneratorstate(my_coro2)
print(state)
# GEN_CLOSED

一、首先inspect.getgeneratorstate 函数指明,处于 GEN_CREATED 状态(即协程未启动)。

二、向前执行协程到第一个 yield 表达式,打印 -> Started: a = 14消息,而后产出(yield)a 的值,而且暂停,等待为 b 赋值。

三、getgeneratorstate 函数指明,处于 GEN_SUSPENDED 状态(即协程在 yield 表达式处暂停)。

四、把数字 28 发给暂停的协程;计算 yield 表达式,获得 28,而后把那个数绑定给 b,打印 -> Received: b = 28 消息,

      而后产出(yield) a + b 的值(42),而且协程暂停,等待为 c 赋值。

五、把数字 99 发给暂停的协程;计算 yield 表达式,获得 99,而后把那个数绑定给 c,打印 -> Received: c = 99 消息,

     而后协程终止,致使生成器对象抛出 StopIteration 异常。

六、最后getgeneratorstate 函数指明,处于 GEN_CLOSED 状态(即协程执行结束)。

 

关键的一点是,协程在 yield 关键字所在的位置暂停执行。前面说过,在赋值语句中,= 右边的代码在赋值以前执行。

所以,对于 b =yield a 这行代码来讲,等到客户端代码再激活协程时才会设定 b 的值

这种行为要花点时间才能习惯,不过必定要理解,这样才能弄懂异步编程中 yield 的做用(后文探讨)。

 

simple_coro2 协程的执行过程分为 3 个阶段,以下图所示。

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

二、调用 my_coro2.send(28),把 28 赋值给 b,打印第二个消息,而后执行 yield a + b,产出数字 42。

三、调用 my_coro2.send(99),把 99 赋值给 c,打印第三个消息,协程终止。

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

 

4、示例:使用协程计算平均值

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

一、这个无限循环代表,只要调用方不断把值发给这个协程,它就会一直接收值,而后生成结果。

仅当调用方在协程上调用 .close() 方法,或者没有对协程的引用而被垃圾回收程序回收时,这个协程才会终止。

二、这里的 yield 表达式用于暂停执行协程,把结果发给调用方;还用于接收调用方后面发给协程的值,恢复无限循环。

使用协程的好处是,total 和 count 声明为局部变量便可,无需使用实例属性或闭包在屡次调用之间保持上下文。

 

使用averager协程:

coro_avg = averager()
next(coro_avg)
avg = coro_avg.send(10)
print(avg)   #10.0
avg = coro_avg.send(30)
print(avg)   #20.0 
avg = coro_avg.send(5)
print(avg)   #15.0

一、建立协程对象。

二、调用 next 函数,预激协程。

三、计算平均值:屡次调用 .send(...) 方法,产出当前的平均值。

 

在上述示例中,调用 next(coro_avg) 函数后,协程会向前执行到 yield 表达式,产出 average 变量的初始值——None,所以不会出如今控制台中。

此时,协程在 yield 表达式处暂停,等到调用方发送值。

coro_avg.send(10) 那一行发送一个值,激活协程,把发送的值赋给 term,并更新 total、count 和 average 三个变量的值,

而后开始 while 循环的下一次迭代,产出 average 变量的值,等待下一次为 term 变量赋值。

 

5、预激协程的装饰器

若是不预激,那么协程没什么用。调用 my_coro.send(x) 以前,记住必定要调用 next(my_coro)。

为了简化协程的用法,有时会使用一个预激装饰器。

示例:预激协程的装饰器

from functools import wraps

def coroutine(func):
    '''装饰器:向前执行到第一个`yield`表达式,预激`func`'''
    @wraps(func)
    def primer(*args, **kwargs):     #1
        gen = func(*args, **kwargs)  #2
        next(gen)    #3
        return gen   #4
    return primer

一、简单说下装饰器,假若有个名为 decorate 的装饰器:

@decorate
def target():
  print('running target()')

     上述代码的效果与下述写法同样:

def target():
    print('running target()')

target
= decorate(target)

 因此示例中把被装饰的生成器函数替换成这里的 primer 函数;调用 primer 函数时,返回预激后的生成器。

二、调用被装饰的函数,获取生成器对象。

三、预激生成器。

四、返回生成器。

 

下面展现 @coroutine 装饰器的用法:

"""
用于计算移动平均值的协程
    >>> coro_avg = averager()    #1
    >>> from inspect import getgeneratorstate
    >>> getgeneratorstate(coro_avg)   #2
    'GEN_SUSPENDED'
    >>> coro_avg.send(10)   #3
    10.0
    >>> coro_avg.send(30)
    20.0
    >>> coro_avg.send(5)
    15.0
"""

from coroutil import coroutine #4 @coroutine #5 def averager(): #6 total = 0.0 count = 0 average = None while True: term = yield average total += term

一、调用 averager() 函数建立一个生成器对象,在 coroutine 装饰器的 primer 函数中已经预激了这个生成器。

二、getgeneratorstate 函数指明,处于 GEN_SUSPENDED 状态,所以这个协程已经准备好,能够接收值了。

三、能够当即开始把值发给 coro_avg——这正是 coroutine 装饰器的目的。

四、导入 coroutine 装饰器。

五、把装饰器应用到 averager 函数上。

六、函数的定义体

 

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

Python 3.4 标准库里的 asyncio.coroutine 装饰器不会预激协程,所以能兼容 yield from 句法。

 

6、终止协程和异常处理

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

示例:未处理的异常会致使协程终止

>>> from coroaverager1 import averager
>>> coro_avg = averager()
>>> coro_avg.send(40) #
40.0
>>> coro_avg.send(50)
45.0
>>> coro_avg.send('spam') #
Traceback (most recent call last):
    ...
TypeError: unsupported operand type(s) for +=: 'float' and 'str'
>>> coro_avg.send(60) #
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
StopIteration

一、使用 @coroutine 装饰器装饰的 averager 协程,能够当即开始发送值。

二、发送的值不是数字,致使协程内部有异常抛出。

三、因为在协程内没有处理异常,协程会终止。若是试图从新激活协程,会抛出 StopIteration 异常。

 

出错的缘由是,发送给协程的 'spam' 值不能加到 total 变量上。

示例暗示了终止协程的一种方式:发送某个哨符值,让协程退出。

 

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

这两个方法是 throw 和 close。

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

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

generator.close()

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

 

下面举例说明如何使用 close 和 throw 方法控制协程。

示例:学习在协程中处理异常的测试代码

class DemoException(Exception):
    """为此次演示定义的异常类型"""

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

一、特别处理 DemoException 异常。

二、若是没有异常,那么显示接收到的值。

三、这一行永远不会执行,由于只有未处理的异常才会停止那个无限循环,而一旦出现未处理的异常,协程会当即终止。

 

实验1 :激活和关闭 demo_exc_handling,没有异常

exc_coro = demo_exc_handling()
next(exc_coro)     # -> coroutine started.
exc_coro.send(11)  # -> coroutine received: 11
exc_coro.send(22)  # -> coroutine received: 22
exc_coro.close()
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))  # GEN_CLOSED

若是把 DemoException 异常传入 demo_exc_handling 协程,它会处理,而后继续运行,以下面的 示例2 所示。

 

实验2 :把 DemoException 异常传入 demo_exc_handling 不会致使协程停止

exc_coro = demo_exc_handling()
next(exc_coro)     # -> coroutine started.
exc_coro.send(11)  # -> coroutine received: 11
exc_coro.throw(DemoException) # *** DemoException handled. Continuing...

from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))  # GEN_SUSPENDED

可是,若是传入协程的异常没有处理,协程会中止,即状态变成'GEN_CLOSED'。

 

实验3 :若是没法处理传入的异常,协程会终止

exc_coro = demo_exc_handling()
next(exc_coro)     # -> coroutine started.
exc_coro.send(11)  # -> coroutine received: 11
exc_coro.throw(ZeroDivisionError) # Traceback (most recent call last)...ZeroDivisionError

from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))  # 因为上面异常致使程序终止,但状态是GEN_CLOSED

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

 

实验4:使用 try/finally 块在协程终止时执行操做

def demo_exc_handling():
    print('-> coroutine started.')
    try:
        while True:
            try:
                x = yield
            except DemoException:  
                print('*** DemoException handled. Continuing...')
            else:  
                print('-> coroutine received: {!r}'.format(x))
    finally:
        print('-> coroutine ending')

Python 3.3 引入 yield from 结构的主要缘由之一与把异常传入嵌套的协程有关。另外一个缘由是让协程更方便地返回值。

 

7、让协程返回值

下面的示例是 averager 协程的不一样版本,这一版会返回结果。为了说明如何返回值,每次激活协程时不会产出移动平均值。

这么作是为了强调某些协程不会产出值,而是在最后返回一个值(一般是某种累计值)

示例中的 averager 协程返回的结果是一个 namedtuple,两个字段分别是项数(count)和平均值(average)。

我本能够只返回平均值,可是返回一个元组能够得到累积数据的另外一个重要信息——项数。

示例:定义一个求平均值的协程,让它返回一个结果

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    #1
        total += term
        count += 1
        average = total / count
    return Result(count, average)   #2

一、为了返回值,协程必须正常终止;所以,这一版 averager 中有个条件判断,以便退出累计循环。

二、返回一个 namedtuple,包含 count 和 average 两个字段。在 Python 3.3 以前,若是生成器返回值,解释器会报句法错误。

 

示例:如何使用新版averager ()

coro_avg = averager()
next(coro_avg)
coro_avg.send(10)    #1
coro_avg.send(30)
coro_avg.send(6.5)
coro_avg.send(None)  #2

Traceback (most recent call last):
  File "...", line 24, in <module>
    coro_avg.send(None)
StopIteration: Result(count=3, average=15.5)

一、这一版不产出值。

二、发送 None 会终止循环,致使协程结束,返回结果。一如既往,生成器对象会抛出StopIteration 异常。

     异常对象的 value 属性保存着返回的值。

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

           这样作有点不合常理,可是能保留生成器对象的常规行为——耗尽时抛出StopIteration 异常。

 

示例:捕获 StopIteration 异常,获取 averager 返回的值

coro_avg = averager()
next(coro_avg)
coro_avg.send(10)    #1
coro_avg.send(30)
coro_avg.send(6.5)
try:
    coro_avg.send(None)
except StopIteration as exc:
    result = exc.value
print(result)

获取协程的返回值虽然要绕个圈子,但这是 PEP 380 定义的方式,当咱们意识到这一点以后就说得通了:yield from 结构会在内部自动捕获 StopIteration 异常。

这种处理方式与 for 循环处理 StopIteration 异常的方式同样:循环机制使用用户易于理解的方式处理异常。

对 yield from 结构来讲,解释器不只会捕获 StopIteration 异常,还会把value 属性的值变成 yield from 表达式的值(=号 左边的)

惋惜,咱们没法在控制台中使用交互的方式测试这种行为,由于在函数外部使用 yield from(以及 yield)会致使句法出错。

 

8、使用yield from

首先要知道,yield from 是全新的语言结构。它的做用比 yield 多不少,所以人们认为继续使用那个关键字多少会引发误解。

在其余语言中,相似的结构使用 await 关键字,这个名称好多了,由于它传达了相当重要的一点:

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

该书前面内容说过,yield from 可用于简化 for 循环中的 yield 表达式。例如:

def gen():
    for c in 'AB':
        yield c
    for i in range(1, 3):
        yield i

li = [n for n in gen()]
print(li)  

输出:  ['A', 'B', 1, 2]

能够改写为:

def gen():
    yield from 'AB'
    yield from range(1, 3)

li = [n for n in gen()]
print(li)

输出:  ['A', 'B', 1, 2]

 

示例:使用 yield from 连接可迭代的对象

def chain(*iterables):
    for it in iterables:
        yield from it

s = 'ABC'
t = tuple(range(3))
li = list(chain(s, t))
print(li)
# ['A', 'B', 'C', 0, 1, 2]

注意: s(字符串)和t(元祖)都是可迭代的对象,生成器也是可迭代的对象!

 

在 Beazley 与 Jones 的《Python Cookbook(第 3 版)中文版》一书中,“4.14 扁平化处理套型的序列”一节有个稍微复杂(不过更有用)的 yield from 示例:

# Example of flattening a nested sequence using subgenerators

from collections import Iterable

def flatten(items, ignore_types=(str, bytes)):
    for x in items:
        if isinstance(x, Iterable) and not isinstance(x, ignore_types):
            yield from flatten(x)
        else:
            yield x

items = [1, 2, [3, 4, [5, 6], 7], 8]

# Produces 1 2 3 4 5 6 7 8
for x in flatten(items):
    print(x)

items = ['Dave', 'Paula', ['Thomas', 'Lewis']]
for x in flatten(items):
    print(x)

 

yield from x 表达式对 x 对象所作的第一件事是,调用 iter(x),从中获取迭代器。所以,x 能够是任何可迭代的对象。

 

但是,若是 yield from 结构惟一的做用是替代产出值的嵌套 for 循环,这个结构颇有可能不会添加到 Python 语言中。

yield from 结构的本质做用没法经过简单的可迭代对象说明,而要发散思惟,使用嵌套的生成器

所以,引入 yield from 结构的 PEP 380 才起了“Syntax for Delegating to a Subgenerator”(“把职责委托给子生成器的句法”)这个标题。

 

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

 

若想使用 yield from 结构,就要大幅改动代码。为了说明须要改动的部分,PEP 380 使用了一些专门的术语。

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

二、子生成器: 从 yield from 表达式中 <iterable> 部分获取的生成器。

                        这就是 PEP 380 的标题(“Syntax for Delegating to a Subgenerator”)中所说的“子生成器”(subgenerator)。

三、调用方:PEP 380 使用“调用方”这个术语指代调用委派生成器的客户端代码。在不一样的语境中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,由于它调用了子生成器)区分开。

 

示例:使用 yield from 计算平均值并输出统计报告

# BEGIN YIELD_FROM_AVERAGER
from collections import namedtuple

Result = namedtuple('Result', 'count average')


# the subgenerator
def averager():  # <1>
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield  # <2>
        if term is None:  # <3>
            break
        total += term
        count += 1
        average = total/count
    return Result(count, average)  # <4>


# the delegating generator
def grouper(results, key):  # <5>
    while True:  # <6>
        results[key] = yield from averager()  # <7>


# the client code, a.k.a. the caller
def main(data):  # <8>
    results = {}
    for key, values in data.items():
        group = grouper(results, key)  # <9>
        next(group)  # <10>
        for value in values:
            group.send(value)  # <11>
        group.send(None)  # important! <12>

    # print(results)  # uncomment to debug
    report(results)


# output report
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))


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)

# END YIELD_FROM_AVERAGER

程序输出:

9 boys averaging 40.42kg
9 boys averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m

一、做为子生成器使用;

二、main函数中客户代码发送的各个值绑定到这里的term变量上;

三、相当重要的终止条件,若是不这么作,使用yield from调用这个协程的生成器会永远阻塞;

四、返回的Result会成为grouper函数中yield from表达式的值(=号左边的);

五、grouper是委派生成器;

六、这个循环每次迭代时会新建一个averager实例;每一个实例都是做为协程使用的生成器对象;

七、grouper发送的每一个值都会经由yield from处理,经过管道传给averager实例。averager实例运行完毕后,返回的值绑定到result[key]上。

      while循环会不断建立averager实例,处理更多的值。

八、main函数是客户端代码,用PE380的术语来讲,是“调用方”,是驱动一切的函数。

九、group 是调用 grouper 函数获得的生成器对象,传给 grouper 函数的第一个参数是results,用于收集结果;第二个参数是某个键。group 做为协程使用。

十、预激 group 协程。

十一、把各个 value 传给 grouper。传入的值最终到达 averager 函数中 term = yield 那一行;grouper 永远不知道传入的值是什么。

十二、把 None 传入 grouper,致使当前的 averager 实例终止,也让 grouper 继续运行,再建立一个 averager 实例,处理下一组值。

       注释——“重要!”,强调这行代码(group.send(None))相当重要:终止当前的 averager 实例,开始执行下一个。

       若是注释掉那一行,这个脚本不会输出任何报告。此时,把 main 函数靠近末尾的print(results) 那行的注释去掉,你会发现,results 字典是空的。

 

下图将示例中各个相关的部分标识出来了:

委派生成器在 yield from 表达式处暂停时,调用方能够直接把数据发给子生成器,子生成器再把产出的值发给调用方。

子生成器返回以后,解释器会抛出StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复

 

下面简要说明示例的运做方式,还会说明把 main 函数中调用 group.send(None)那一行代码(带有“重要!”注释的那一行)去掉会发生什么事。

一、外层 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 from 调用另外一个子生成器,以此类推。最终,这个链条要以一个只使用 yield表达式的简单生成器结束;不过,也能以任何可迭代的对象结束,如示例所示。

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

下面综述 PEP 380 对 yield from 结构的正式说明。

 

9、yield from的意义

PEP380 草稿中有这样一段话:“把迭代器看成生成器使用,至关于把子生成器的定义体内联在 yield from 表达式中。此外,子生成器能够执行 return 语句,返回一个值,而返回的值会成为 yield from 表达式的值。”

PEP 380 中已经没有这段宽慰人心的话,由于没有涵盖全部极端状况。

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

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

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

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

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

 

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

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

二、若是把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么在子生成器上调用 close() 方法,若是它有的话。若是调用 close() 方法致使异常抛出,那么异常会向上冒泡,传给委派生成器;不然,委派生成器抛出GeneratorExit 异常。

yield from 的具体语义很难理解,尤为是处理异常的那两点。若想仔细研究,最好将其简化,只涵盖 yield from 最基本且最多见的用法。

 

假设 yield from 出如今委派生成器中。客户端代码驱动着委派生成器,而委派生成器驱动着子生成器。那么,为了简化涉及到的逻辑,咱们假设客户端没有在委派生成器上调用.throw(...) 或.close() 方法。此外,咱们还假设子生成器不会抛出异常,而是一直运行到终止,让解释器抛出 StopIteration 异常。下面来看一下在这个简化的美满世界中,yield from 是如何运做的。

请看示例,那里列出的代码是委派生成器的定义体中下面这一行代码的扩充:

RESULT = yield from EXPR

示例:简化的伪代码,等效于委派生成器中的 RESULT = yield from EXPR语句(这里针对的是最简单的状况:不支持 .throw(...) 和 .close() 方法,并且只处理 StopIteration 异常)

_i = iter(EXPR)  #1
try:
    _y = next(_i)  #2
except StopIteration as _e:
    _r = _e.value  #3
else:
    while 1:  #4
        _s = yield _y  #5
        try:
            _y = _i.send(_s)  #6
        except StopIteration as _e:  #7
            _r = _e.value
        break
        
RESULT = _r

一、EXPR能够是任何可迭代的对象,由于获取迭代器_i(这是子生成器)使用的是iter()函数;

二、预激子生成器,结果保存在_y中,做为产出的第一个值;

三、若是抛出StopIteration异常,获取异常对象的value属性,赋值给_r——这是最简单状况下的返回值(RESULT)

四、运行这个循环时,委派生成器会阻塞,只做为调用方和子生成器之间的通道;

五、产出子生成器当前产出的元素;等待调用方发送_s中保存的值。注意,这个代码清单中只有这一个yield表达式;

六、尝试让子生成器向前执行,转发调用方发送的_s;

七、若是子生成器抛出StopIteration异常,获取value属性的值,赋值给_r,而后退出循环,让委派生成器恢复运行;

八、返回的结果是(RESULT)是_r,即整个yield from表达式的值。

 

在这段简化的伪代码中,我保留了 PEP 380 中那段伪代码使用的变量名称。这些变量是:

_i(迭代器)

  子生成器

_y(产出的值)

  子生成器产出的值

_r(结果)

  最终的结果(即子生成器运行结束后 yield from 表达式的值)

_s(发送的值)

  调用方发给委派生成器的值,这个值会转发给子生成器

_e(异常)

  异常对象(在这段简化的伪代码中始终是 StopIteration 实例)

 

除了没有处理 .throw(...) 和 .close() 方法以外,这段简化的伪代码还在子生成器上调用 .send(...) 方法,以此达到客户调用next() 函数或 .send(...) 方法的目的。首次阅读时不要担忧这些细微的差异。前面说过,即便 yield from 结构只作上一节示例展现的事情,也依旧能正常运行。

 

可是,现实状况要复杂一些,由于要处理客户对 .throw(...) 和 .close() 方法的调用,而这两个方法执行的操做必须传入子生成器。此外,子生成器可能只是纯粹的迭代器,不支持 .throw(...) 和 .close() 方法,所以 yield from 结构的逻辑必须处理这种状况。若是子生成器实现了这两个方法,而在子生成器内部,这两个方法都会触发异常抛出,这种状况也必须由 yield from 机制处理。调用方可能会平白无故地让子生成器本身抛出异常,实现 yield from 结构时也必须处理这种状况。最后,为了优化,若是调用方调用 next(...) 函数或 .send(None) 方法,都要转交职责,在子生成器上调用next(...) 函数;仅当调用方发送的值不是 None 时,才使用子生成器的 .send(...) 方法。

为了方便对比,下面列出 PEP 380 中扩充 yield from 表达式的完整伪代码,并且加上了带标号的注解。下面示例中的代码是一字不差复制过来的,只有标注是我本身加的。

再次说明,示例中的代码是委派生成器的定义体中下面这一个语句的扩充:

RESULT = yield from EXPR

示例 :伪代码,等效于委派生成器中的 RESULT = yield from EXPR 语句

_i = iter(EXPR)  #1
try:
    _y = next(_i)  #2
except StopIteration as _e:
    _r = _e.value  #3
else:
    while 1:  #4
        try:
            _s = yield _y  #5
        except GeneratorExit as _e:  #6
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e:  #7
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:  #8
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:  #9
            try:  #10
                if _s is None:  #11
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:   #12
                _r = _e.value
                break

RESULT = _r   #13

一、 EXPR 能够是任何可迭代的对象,由于获取迭代器 _i(这是子生成器)使用的是iter() 函数。

二、预激子生成器;结果保存在 _y 中,做为产出的第一个值。

三、若是抛出 StopIteration 异常,获取异常对象的 value 属性,赋值给 _r——这是最简单状况下的返回值(RESULT)。

四、运行这个循环时,委派生成器会阻塞,只做为调用方和子生成器之间的通道。

五、产出子生成器当前产出的元素;等待调用方发送 _s 中保存的值。这个代码清单中只有这一个 yield 表达式。

六、这一部分用于关闭委派生成器和子生成器。由于子生成器能够是任何可迭代的对象,因此可能没有 close 方法。

七、这一部分处理调用方经过 .throw(...) 方法传入的异常。一样,子生成器能够是迭代器,从而没有 throw 方法可调用——这种状况会致使委派生成器抛出异常。

八、若是子生成器有 throw 方法,调用它并传入调用方发来的异常。子生成器可能会处理传入的异常(而后继续循环);可能抛出 StopIteration 异常(从中获取结果,赋值给_r,循环结束);还可能不处理,而是抛出相同的或不一样的异常,向上冒泡,传给委派生成器。

九、若是产出值时没有异常……

十、尝试让子生成器向前执行……

十一、若是调用方最后发送的值是 None,在子生成器上调用 next 函数,不然调用 send 方法。

十二、若是子生成器抛出 StopIteration 异常,获取 value 属性的值,赋值给 _r,而后退出循环,让委派生成器恢复运行。

1三、返回的结果(RESULT)是 _r,即整个 yield from 表达式的值。

这段 yield from 伪代码的大多数逻辑经过六个 try/except 块实现,并且嵌套了四层,所以有点难以阅读。此外,用到的其余流程控制关键字有一个 while、一个 if 和一个yield。找到 while 循环、yield 表达式以及 next(...) 函数和 .send(...) 方法调用,这些代码有助于对 yield from 结构的运做方式有个总体的了解。

 

就在示例所列伪代码的顶部,有行代码(标号❷)揭示了一个重要的细节:要预激子生成器。 这代表,用于自动预激的装饰器与 yield from 结构不兼容。

 

仔细研究扩充的伪代码可能没什么用——这与你的学习方式有关。显然,分析真正使用yield from 结构的代码要比深刻研究实现这一结构的伪代码更有好处。不过,我见过的yield from 示例几乎都使用 asyncio 模块作异步编程,所以要有有效的事件循环才能运行。

 

下面分析一个使用协程的经典案例:仿真编程。这个案例没有展现 yield from 结构的用法,可是揭示了如何使用协程在单个线程中管理并发活动。

 

 10、使用案例:使用协程作离散事件仿真

 协程是 asyncio 包的基础构建。经过仿真系统能说明如何使用协程代替线程实现并发的活动,并且对理解asyncio 包有极大的帮助。

一、离散事件仿真简介

离散事件仿真(Discrete Event Simulation,DES)是一种把系统建模成一系列事件的仿真类型。在离散事件仿真中,仿真“钟”向前推动的量不是固定的,而是直接推动到下一个事件模型的模拟时间。假如咱们抽象模拟出租车的运营过程,其中一个事件是乘客上车,下一个事件则是乘客下车。无论乘客坐了 5 分钟仍是 50 分钟,一旦乘客下车,仿真钟就会更新,指向这次运营的结束时间。使用离散事件仿真能够在不到一秒钟的时间内模拟一年的出租车运营过程。这与连续仿真不一样,连续仿真的仿真钟以固定的量(一般很小)不断向前推动。

显然,回合制游戏就是离散事件仿真的例子:游戏的状态只在玩家操做时变化,并且一旦玩家决定下一步怎么走了,仿真钟就会冻结。而实时游戏则是连续仿真,仿真钟一直在运行,游戏的状态在一秒钟以内更新不少次,所以反应慢的玩家特别吃亏。这两种仿真类型都能使用多线程或在单个线程中使用面向事件的编程技术(例如事件循环驱动的回调或协程)实现。能够说,为了实现连续仿真,在多个线程中处理实时并行的操做更天然。而协程刚好为实现离散事件仿真提供了合理的抽象。

在仿真领域,进程这个术语指代模型中某个实体的活动,与操做系统中的进程无关。仿真系统中的一个进程可使用操做系统中的一个进程实现,可是一般会使用一个线程或一个协程实现。


二、出租车队运营仿真

仿真程序 taxi_sim.py 会建立几辆出租车,每辆车会拉几个乘客,而后回家。出租车首先驶离车库,四处徘徊,寻找乘客;拉到乘客后,行程开始;乘客下车后,继续四处徘徊。四处徘徊和行程所用的时间使用指数分布生成。为了让显示的信息更加整洁,时间使用取整的分钟数,不过这个仿真程序也能使用浮点数表示耗时。 每辆出租车每次的状态变化都是一个事件。下图 是运行这个程序的输出示例。


图 1:运行 taxi_sim.py 建立 3 辆出租车的输出示例。-s 3 参数设置随机数生成器的种子,这样在调试和演示时能够重复运行程序,输出相同的结果。不一样颜色的箭头表示不一样出租车的行程.

图中最值得注意的一件事是,3 辆出租车的行程是交叉进行的。那些箭头是我加上的,为的是让你看清各辆出租车的行程:箭头从乘客上车时开始,到乘客下车后结束。有了箭头,能直观地看出如何使用协程管理并发的活动。


图中还有几件事值得注意。

一、出租车每隔 5 分钟从车库中出发。
二、0 号出租车 2 分钟后拉到乘客(time=2),1 号出租车 3 分钟后拉到乘客(time=8),2 号出租车 5 分钟后拉到乘客(time=15)。
三、0 号出租车拉了两个乘客(紫色箭头):第一个乘客从 time=2 时上车,到 time=18时下车;第二个乘客从 time=28 时上车,到 time=65 时下车——这是这次仿真中最长的行程。
四、1 号出租车拉了四个乘客(绿色箭头),在 time=110 时回家。
五、2 号出租车拉了六个乘客(红色箭头),在 time=109 时回家。这辆车最后一次行程从 time=97 时开始,只持续了一分钟。
六、1 号出租车的第一次行程从 time=8 时开始,在这个过程当中 2 号出租车离开了车库(time=10),并且完成了两次行程(那两个短的红色箭头)。
七、在这次运行示例中,全部排定的事件都在默认的仿真时间内(180 分钟)完成;最后一次事件发生在 time=110 时。

本章只会列出taxi_sim.py中与协程相关的部分。真正重要的函数只有两个:taxi_process(一个协程),以及执行仿真主循环的 Simulator.run方法。

示例 16-20 是 taxi_process 函数的代码。这个协程用到了别处定义的两个对象:compute_delay 函数,返回单位为分钟的时间间隔;Event 类,一个namedtuple,定义方式以下:

Event = collections.namedtuple('Event', 'time proc action')

 

在 Event 实例中,time 字段是事件发生时的仿真时间,proc 字段是出租车进程实例的编号,action 字段是描述活动的字符串。
下面逐行分析示例 16-20 中的 taxi_process 函数。

示例 16-20 taxi_sim.py:taxi_process 协程,实现各辆出租车的活动

def taxi_process(ident, trips, start_time=0):  #1
    """每次改变状态时建立事件,把控制权让给仿真器"""
    time = yield Event(start_time, ident, 'leave garage')  #2
    for i in range(trips):  #3
        time = yield Event(time, ident, 'pick up passenger')  #4
        time = yield Event(time, ident, 'drop off passenger')  #5

    yield Event(time, ident, 'going home')  #6
    # 出租车进程结束   #7

一、每辆出租车调用一次 taxi_process 函数,建立一个生成器对象,表示各辆出租车的运营过程。ident 是出租车的编号(如上述运行示例中的 0、一、2);trips 是出租车回家以前的行程数量;start_time 是出租车离开车库的时间。

二、产出的第一个 Event 是 'leave garage'。执行到这一行时,协程会暂停,让仿真主循环着手处理排定的下一个事件。须要从新激活这个进程时,主循环会发送(使用 send方法)当前的仿真时间,赋值给 time。

三、每次行程都会执行一遍这个代码块。

四、产出一个 Event 实例,表示拉到乘客了。协程在这里暂停。须要从新激活这个协程时,主循环会发送(使用 send 方法)当前的时间。

五、产出一个 Event 实例,表示乘客下车了。协程在这里暂停,等待主循环发送时间,而后从新激活。

六、指定的行程数量完成后,for 循环结束,最后产出 'going home' 事件。此时,协程最后一次暂停。仿真主循环发送时间后,协程从新激活;不过,这里没有把产出的值赋值给变量,由于用不到了。

七、协程执行到最后时,生成器对象抛出 StopIteration 异常。

 

为了实例化 Simulator 类,taxi_sim.py 脚本的 main 函数构建了一个 taxis 字典,以下所示:

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

DEPARTURE_INTERVAL 的值是 5;若是 num_taxis 的值与前面的运行示例同样也是 3,这三行代码的做用与下述代码同样:

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)    

所以,taxis 字典的值是三个参数不一样的生成器对象。例如,1 号出租车从start_time=5 时开始,寻找四个乘客。构建 Simulator 实例只需这个字典参数。Simulator.__init__ 方法如示例 16-22 所示。Simulator 类的主要数据结构以下。

self.events
  PriorityQueue 对象,保存 Event 实例。元素能够放进(使用 put 方法)PriorityQueue 对象中,而后按 item[0](即 Event 对象的 time 属性)依序取出(使用 get 方法)。

self.procs
  一个字典,把出租车的编号映射到仿真过程当中激活的进程(表示出租车的生成器对象)。这个属性会绑定前面所示的 taxis 字典副本。

  示例 16-22 taxi_sim.py:Simulator 类的初始化方法

class Simulator:
    def __init__(self, procs_map):
        self.events = queue.PriorityQueue() #1
        self.procs = dict(procs_map) #2

一、保存排定事件的 PriorityQueue 对象,按时间正向排序。

二、获取的 procs_map 参数是一个字典(或其余映射),但是又从中构建一个字典,建立本地副本,由于在仿真过程当中,出租车回家后会从 self.procs 属性中移除,而咱们不想修改用户传入的对象。

优先队列是离散事件仿真系统的基础构件:建立事件的顺序不定,放入这种队列以后,能够按照各个事件排定的时间顺序取出。例如,可能会把下面两个事件放入优先队列:

Event(time=14, proc=0, action='pick up passenger')
Event(time=11, proc=1, action='pick up passenger')

这两个事件的意思是,0 号出租车 14 分钟后拉到第一个乘客,而 1 号出租车(time=10时出发)1 分钟后(time=11)拉到乘客。若是这两个事件在队列中,主循环从优先队列中获取的第一个事件将是 Event(time=11, proc=1, action='pick uppassenger')。

下面分析这个仿真系统的主算法——Simulator.run 方法。在 main 函数中,实例化Simulator 类以后当即就调用了这个方法,以下所示:

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

Simulator 类带有注解的代码清单在示例 16-23 中,下面先概述 Simulator.run 方法实现的算法。

(1) 迭表明示各辆出租车的进程。
  a. 在各辆出租车上调用 next() 函数,预激协程。这样会产出各辆出租车的第一个事件。
  b. 把各个事件放入 Simulator 类的 self.events 属性(队列)中。

(2) 知足 sim_time < end_time 条件时,运行仿真系统的主循环。
  a. 检查 self.events 属性是否为空;若是为空,跳出循环。
  b. 从 self.events 中获取当前事件(current_event),即 PriorityQueue 对象中时间值最小的 Event 对象。
  c. 显示获取的 Event 对象。
  d.获取 current_event 的 time 属性,更新仿真时间。
  e.把时间发给 current_event 的 proc 属性标识的协程,产出下一个事件(next_event)。
  f.把 next_event 添加到 self.events 队列中,排定 next_event。

  示例 16-23 taxi_sim.py:Simulator,一个简单的离散事件仿真类;关注的重点是run 方法

# -*- coding: utf-8 -*-

import collections, queue, time, random

NUM_TAXIS = 3
DEFAULT_END_TIME = 180
SEARCH_DURATION = 5
TRIP_DURATION = 20
DEPARTURE_INTERVAL = 5

Event = collections.namedtuple('Event', ['time', 'proc', 'action'])

def taxi_process(ident, trips, start_time=0):  #1
    """每次改变状态时建立事件,把控制权让给仿真器"""
    time = yield Event(start_time, ident, 'leave garage')  #2
    for i in range(trips):  #3
        time = yield Event(time, ident, 'pick up passenger')  #4
        time = yield Event(time, ident, 'drop off passenger')  #5

    yield Event(time, ident, 'going home')  #6
    # 出租车进程结束   #7
taxis = {i: taxi_process(i, (i + 1) * 2, i * DEPARTURE_INTERVAL)
                            for i in range(NUM_TAXIS)}

class Simulator(object):

    def __init__(self, procs_map):
        self.events = queue.PriorityQueue()
        self.procs = dict(procs_map)

    def run(self, end_time):  #1
        """排定并显示事件,直到时间结束"""
        # 排定各辆出租车的第一个事件
        for _, proc in sorted(self.procs.items()):  #2
            first_event = next(proc)  #3
            self.events.put(first_event)  #4

        # 这个仿真系统的主循环
        sim_time = 0  #5
        while sim_time < DEFAULT_END_TIME:  #6
            if self.events.empty():  #7
                print('*** end of events ***')
                break

            current_event = self.events.get()  #8  在self.events中取出event后,该event就会从queue中删除
            sim_time, proc_id, previous_action = current_event  #9
            print('taxi: ', proc_id, proc_id * '  ', current_event)  #10
            active_proc = self.procs[proc_id]  #11
            next_time = sim_time + compute_duration(previous_action)  #12
            try:
                next_event = active_proc.send(next_time)  #13
            except StopIteration:
                del self.procs[proc_id]  #14
            else:
                self.events.put(next_event)  #15
        else:
            msg = '*** end of simulation time: {} events pending ***'
            print(msg.format(self.event.qsize()))

def compute_duration(previous_action):
    """使用指数分布计算操做的耗时"""
    if previous_action in ['leave garage', 'drop off passenger']:
        # 新状态是四处徘徊
        interval = SEARCH_DURATION
    elif previous_action == 'pick up passenger':
        # 新状态是行程开始
        interval = TRIP_DURATION
    elif previous_action == 'going home':
        interval = 1
    else:
        raise ValueError('Unknown previous_action: %s' % previous_action)
    return int(random.expovariate(1 / interval)) + 1

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

一、run 方法只须要仿真结束时间(end_time)这一个参数。
二、使用 sorted 函数获取 self.procs 中按键排序的元素;用不到键,所以赋值给 _。
三、 调用 next(proc) 预激各个协程,向前执行到第一个 yield 表达式,作好接收数据的准备。产出一个 Event 对象。
四、 把各个事件添加到 self.events 属性表示的 PriorityQueue 对象中。如示例16-20中的运行示例,各辆出租车的第一个事件是 'leave garage'。
五、 把 sim_time 变量(仿真钟)归零。
六、这个仿真系统的主循环:sim_time 小于 end_time 时运行。
七、若是队列中没有未完成的事件,退出主循环。
八、 获取优先队列中 time 属性最小的 Event 对象;这是当前事件(current_event)。
九、拆包 Event 对象中的数据。这一行代码会更新仿真钟 sim_time,对应于事件发生时的时间。
十、显示 Event 对象,指明是哪辆出租车,并根据出租车的编号缩进。
十一、从 self.procs 字典中获取表示当前活动的出租车的协程。
十二、调用 compute_duration(...) 函数,传入前一个动做(例如,'pick uppassenger'、'drop off passenger' 等),把结果加到 sim_time 上,计算出下一次活动的时间。
1三、把计算获得的时间发给出租车协程。协程会产出下一个事件(next_event),或者抛出 StopIteration 异常(完成时)。
1四、若是抛出了 StopIteration 异常,从 self.procs 字典中删除那个协程。
1五、 不然,把 next_event 放入队列中。
1六、若是循环因为仿真时间到了而退出,显示待完成的事件数量(有时可能碰巧是零)。

注意,示例 16-23 中的 Simulator.run 方法有两处用到了第 15 章介绍的 else 块,并且都不在 if 语句中。一、主 while 循环有一个 else 语句,报告仿真系统因为到达结束时间而结束,而不是因为没有事件要处理而结束。二、靠近主 while 循环底部那个 try 语句把 next_time 发给当前的出租车进程,尝试获取下一个事件(next_event),若是成功,执行 else 块,把 next_event 放入self.events 队列中。我以为,若是没有这两个 else 块,Simulator.run 方法的代码会有点难以阅读。这个示例的要旨是说明如何在一个主循环中处理事件,以及如何经过发送数据驱动协程。这是 asyncio 包底层的基本思想。

相关文章
相关标签/搜索