如果 Python 书籍有一定的指导作用,那么(协程就是)文档最匮乏、最鲜为人知的 Python 特性,因此表面上看是最无用的特性。
——David Beazley
Python 图书作者
字典为动词“to yield”给出了两个释义:产出和让步。对于 Python 生成器中的 yield 来说,这两个含义都成立。yield item 这行代码会产出一个值,提供给 next(...) 的调用方;此外,还会作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next()。调用方会从生成器中拉取值。
从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数。可是,在协程中,yield 通常出现在表达式的右边(例如,datum = yield),可以产出值,也可以不产出——如果 yield关键字后面没有表达式,那么生成器产出 None。协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是 next(...) 函数。通常,调用方会把值推送给协程。
yield 关键字甚至还可以不接收或传出数据。不管数据如何流动,yield 都是一种流程控制工具,使用它可以实现协作式多任务:协程可以把控制器让步给中心调度程序,从而激活其他的协程。
从根本上把 yield 视作控制流程的方式,这样就好理解协程了。
生成器如何进化成协程
协程的底层架构在“PEP 342—Coroutines via EnhancedGenerators”(https://www.python.org/dev/peps/pep-0342/)中定义,并在Python 2.5(2006 年)实现了。自此之后,yield 关键字可以在表达式中使用,而且生成器 API 中增加了 .send(value) 方法。生成器的调用方可以使用 .send(...) 方法发送数据,发送的数据会成为生成器函数中 yield 表达式的值。因此,生成器可以作为协程使用。协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。
除了 .send(...) 方法,PEP 342 还添加了 .throw(...) 和 .close()方法:前者的作用是让调用方抛出异常,在生成器中处理;后者的作用是终止生成器。
用作协程的生成器的基本行为
举个 🌰 演示协程的用法
1 def simple_coroutine(): # 携程使用生成器函数定义:定义题中有yield关键字 2 print('-> coroutine started') # 如果携程只从客户那里接受数据,那么产出的值是None,这个值是隐式的,因为yield关键字右边没有表达式 3 x = yield 4 print('-> coroutine received:', x) 5 6 my_coro = simple_coroutine() 7 print(my_coro) # 与创建生成器的方式一样,调用函数得到生成器对象 8 next(my_coro) # 首先要调用next(..)函数,因为生成器还没有启动,没在yield语句初暂停,所以一开始无法发送数据 9 10 my_coro.send(10) # 调用这个方法后,携程定义中的yield表但是会出现10,直到下一个yield出现或者终止
以上代码执行的结果为:
<generator object simple_coroutine at 0x102a463b8> -> coroutine started -> coroutine received: 10 Traceback (most recent call last): ........... my_coro.send(10) # 调用这个方法后,携程定义中的yield表但是会出现10,直到下一个yield出现或者终止 StopIteration
协程可以身处四个状态中的一个。当前状态可以使用inspect.getgeneratorstate(...) 函数确定,该函数会返回下述字符串中的一个。
'GEN_CREATED'
等待开始执行。
'GEN_RUNNING'
解释器正在执行。
'GEN_SUSPENDED'
在 yield 表达式处暂停。
'GEN_CLOSED'
执行结束。
因为 send 方法的参数会成为暂停的 yield 表达式的值,所以,仅当协程处于暂停状态时才能调用 send 方法,例如 my_coro.send(10)。不过,如果协程还没激活(即,状态是 'GEN_CREATED'),情况就不同了。因此,始终要调用 next(my_coro) 激活协程——也可以调用my_coro.send(None),效果一样。
如果创建协程对象后立即把 None 之外的值发给它,会出现下述错误:
>>> my_coro = simple_coroutine() >>> my_coro.send(1729) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't send non-None value to a just-started generator
注意错误消息,它表述得相当清楚。最先调用 next(my_coro) 函数这一步通常称为“预激”(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一下 'GEN_CREATED' >>> next(my_coro2) # 向前执行协程到第一个yield表达式,打印-> Started 这个信息以后,然后产出的a的值,并且停止,等待为b赋值 -> Started: a = 14 14 >>> getgeneratorstate(my_coro2) # 查看协程的状态,现在处于GEN_SUSPENDED状态(即协程在yield表达式处暂停) 'GEN_SUSPENDED' >>> my_coro2.send(28) # 把数字28发给暂停的协程,计算yield表达式,得到28,然后绑定给b,产出a + b的值(42),然后协程暂停,等待为c赋值 -> Received: b = 28 42 >>> my_coro2.send(99) # 把数字99发送给暂停的协程,计算yield表达式,得到99,然后把得到的数字绑定给c,然后协程终止。导致生成器抛出StopIteration -> Received: c = 99 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>> getgeneratorstate(my_coro2) # 协程的状态处于GEN_CLOSED状态 'GEN_CLOSED'
关键的一点是,协程在 yield 关键字所在的位置暂停执行。前面说过,在赋值语句中,= 右边的代码在赋值之前执行。因此,对于 b =yield a 这行代码来说,等到客户端代码再激活协程时才会设定 b 的值。这种行为要花点时间才能习惯,不过一定要理解,这样才能弄懂异步编程中 yield 的作用。
simple_coro2 协程的执行过程分为 3 个阶段,如图下图所示。
(1) 调用 next(my_coro2),打印第一个消息,然后执行 yield a,产出数字 14。
(2) 调用 my_coro2.send(28),把 28 赋值给 b,打印第二个消息,然后执行 yield a + b,产出数字 42。
(3) 调用 my_coro2.send(99),把 99 赋值给 c,打印第三个消息,协程终止。
执行 simple_coro2 协程的 3 个阶段(注意,各个阶段都在yield 表达式中结束,而且下一个阶段都从那一行代码开始,然后再把 yield 表达式的值赋给变量)
示例:使用协程计算移动平均值
使用协程的好处是,total 和 count 声明为局部变量即可,无需使用实例属性或闭包在多次调用之间保持上下文。下面的 🌰 是使用averager 协程的 doctest。
coroaverager0.py:定义一个计算移动平均值的协程
1 def averager(): 2 total = 0 3 count = 0 4 average = None 5 while True: # 无限循环一直会不断的把值发送给这个协程,它就会一直接受,然后生成结果 6 # 仅当调用方在协程上调用.close()方法,或者没有对协程引用的时候才会终止 7 term = yield average # 这里的yield表达式用于暂停执行协程,把结果发送给调用方,还用于接受调后面发给协程的值 8 total += term 9 count += 1 10 average = total/count
以上代码执行的结果为:
>>> coro_avg = averager() # 创建协程对象 >>> next(coro_avg) # 调用next函数,预激协程 >>> coro_avg.send(10) # 计算平均值:多次调用send(...)方法,产出当前平均值 10.0 >>> coro_avg.send(30) 20.0 >>> coro_avg.send(5) 15.0
在上述 doctest 中,调用 next(coro_avg) 函数后,协程会向前执行到 yield 表达式,产出 average 变量的初始值——None,因此不会出现在控制台中。此时,协程在 yield 表达式处暂停,等到调用方发送值。coro_avg.send(10) 那一行发送一个值,激活协程,把发送的值赋给 term,并更新 total、count 和 average 三个变量的值,然后开始 while 循环的下一次迭代,产出 average 变量的值,等待下一次为 term 变量赋值。
预激协程的装饰器
如果不预激,那么协程没什么用。调用 my_coro.send(x) 之前,记住一定要调用 next(my_coro)。为了简化协程的用法,有时会使用一个预激装饰器。
1 from functools import wraps 2 from inspect import getgeneratorstate 3 4 5 def coroutine(func): 6 @wraps(func) 7 def primer(*args, **kwargs): # 把被装饰的生成器函数天换成这里的primer函数,调用peimer函数时,返回预激后的生成器 8 gen = func(*args, **kwargs) # 获取生成器对象 9 next(gen) # 预激活 10 return gen # 返回生成器 11 return primer 12 13 14 @coroutine # 预激活装饰器 15 def averager(): 16 total = 0 17 count = 0 18 average = None 19 while True: # 无限循环一直会不断的把值发送给这个协程,它就会一直接受,然后生成结果 20 # 仅当调用方在协程上调用.close()方法,或者没有对协程引用的时候才会终止 21 term = yield average # 这里的yield表达式用于暂停执行协程,把结果发送给调用方,还用于接受调后面发给协程的值 22 total += term 23 count += 1 24 average = total/count 25 26 coro_avg = averager() # 调用averager()函数创建一个生成器对象,在coroutine装饰器的primer函数中已预激活 27 print(getgeneratorstate(coro_avg)) # 查看协程的状态,已经是可以接收值得状态咯 28 print(coro_avg.send(10)) # 给协程发送数据 29 print(coro_avg.send(30)) 30 print(coro_avg.send(5))
以上代码的执行结果为:
GEN_SUSPENDED
10.0
20.0
15.0
终止协程和异常处理
协程中未处理的异常会向上冒泡,传给 next 函数或 send 方法的调用方(即触发协程的对象)
>>> from coroaverager1 import averager >>> coro_avg = averager() >>> coro_avg.send(40) # 使用@corotine装饰器装饰的averager协程,可以立即开始发送值 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) # 由于异常没有处理,so...协程会终止。如果试图重新激活协程,会抛出StopIteration异常 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
出错的原因是,发送给协程的 'spam' 值不能加到 total 变量上
暗示了终止协程的一种方式:发送某个哨符值,让协程退出。内置的 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 异常。生成器抛出的其他异常会向上冒泡,传给调用方。
coro_exc_demo.py:学习在协程中处理异常的测试代码
1 class DemoException(Exception): 2 """为了掩饰定义的异常类型""" 3 4 def demo_exc_handling(): 5 print('-> coroutine startedd') 6 while True: 7 try: 8 x = yield 9 except DemoException: # 特别处理 DemoException 异常 10 print('*** DemoException handled. Continuing...') 11 else: # 没有异常就接收值 12 print('-> coroutine received: {!r}'.format(x)) 13 14 raise RuntimeError('This line should never run.') # while True会不停止的循环,这一样会一直执行
激活和关闭 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 >>> getgeneratorstate(exc_coro) 'GEN_CLOSED'
如果把 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... >>> getgeneratorstate(exc_coro) 'GEN_SUSPENDED'
但是,如果传入协程的异常没有处理,协程会停止,即状态变成'GEN_CLOSED'
>>> 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 >>> getgeneratorstate(exc_coro) 'GEN_CLOSED'
如果不管协程如何结束都想做些清理工作,要把协程定义体中相关的代码放入 try/finally 块中
🌰 coro_finally_demo.py:使用 try/finally 块在协程终止时执行操作
1 class DemoException(Exception): 2 pass 3 4 def demo_finall(): 5 print('-> coroutine started') 6 try: 7 while True: 8 try: 9 x = yield 10 except DemoException: 11 print('*** DemoException handled. Continuing...') 12 else: 13 print('-> coroutine received: {!r}'.format(x)) 14 finally: 15 print('-> coroutine ending')
让协程返回值
下面的 🌰 是 averager 协程的不同版本,这一版会返回结果。为了说明如何返回值,每次激活协程时不会产出移动平均值。这么做是为了强调某些协程不会产出值,而是在最后返回一个值(通常是某种累计值)。
1 from collections import namedtuple 2 3 Result = namedtuple('Result', 'count average') 4 5 def averager(): 6 total = 0.0 7 count = 0 8 average = None 9 while True: 10 term = yield 11 if term is None: # 当send(None)的时候,终止协程 12 break 13 total += term 14 count += 1 15 average = total/count 16 return Result(count, average) # 返回一个namedtuple,包含两个字段count, average
注意:
return 表达式的值会偷偷传给调用方,赋值给 StopIteration异常的一个属性。这样做有点不合常理,但是能保留生成器对象的常规行为——耗尽时抛出 StopIteration 异常。
演示🌰 捕获 StopIteration 异常,获取 averager 返回的值
>>> coro_avg = averager() >>> next(coro_avg) >>> coro_avg.send(10) >>> coro_avg.send(30) >>> coro_avg.send(6.5) >>> try: ... coro_avg.send(None) ... except StopIteration as exc: ... result = exc.value ... >>> result Result(count=3, average=15.5)
使用yield from
首先要知道,yield from 是全新的语言结构。它的作用比 yield 多很多,因此人们认为继续使用那个关键字多少会引起误解。在其他语言中,类似的结构使用 await 关键字,这个名称好多了,因为它传达了至关重要的一点:在生成器 gen 中使用 yield from subgen()时,subgen 会获得控制权,把产出的值传给 gen 的调用方,即调用方可以直接控制 subgen。与此同时,gen 会阻塞,等待 subgen 终止。
举个🌰 yield from 可用于简化 for 循环中的 yield 表达式
1 def gen(): 2 for i in 'AB': 3 yield i 4 5 for i in range(1, 3): 6 yield i 7 8 print(list(gen())) 9 10 11 ''' 12 yield from 版本,可以简化内部的for循环 13 ''' 14 def gen(): 15 yield from 'AB' 16 yield from range(1, 3) 17 18 print(list(gen()))
🌰 使用 yield from 链接可迭代的对象
1 def chain(*iterable): 2 for i in iterable: 3 yield from i 4 5 s = 'ABC' 6 t = tuple(range(1, 5)) 7 r = list(chain(s, t)) 8 print(r)
以上代码执行的结果为:
['A', 'B', 'C', 1, 2, 3, 4]
yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因此,x 可以是任何可迭代的对象。
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。
委派生成器
包含 yield from <iterable> 表达式的生成器函数。
子生成器
从 yield from 表达式中 <iterable> 部分获取的生成器。
调用方
PEP 380 使用“调用方”这个术语指代调用委派生成器的客户端代码。在不同的语境中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,因为它调用了子生成器)区分开。
下图能更好地说明 yield from 结构的用法。图中把该示例中各个相关的部分标识出来了
委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出 StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复
coroaverager3.py 脚本从一个字典中读取虚构的七年级男女学生的体重和身高。例如, 'boys;m' 键对应于 9 个男学生的身高(单位是米),'girls;kg' 键对应于 10 个女学生的体重(单位是千克)。这个脚本把各组数据传给前面定义的 averager 协程,然后生成一个报告,如下所示:
$ python3 coroaverager3.py 9 boys averaging 40.42kg 9 boys averaging 1.39m 10 girls averaging 42.04kg 10 girls averaging 1.43m
🌰 coroaverager3.py:使用 yield from 计算平均值并输出统计报告
1 from collections import namedtuple 2 3 4 Result = namedtuple('Result', 'count average') 5 6 7 # 子生成器 8 def averager(): # 子生成器 9 total = 0.0 10 count = 0 11 average = None 12 while True: 13 term = yield # 通过main函数中的gourp.send()接收到term的值 14 if term is None: # 至关重要的终止条件,告诉协程所有的数据已经结束,结束协程 15 break 16 total += term 17 count += 1 18 average = total/count 19 return Result(count, average) # 返回 grouper 中yield from的值 20 21 22 # 委派生成器 23 def grouper(results, key): # 委派生成器 24 while True: # 每次循环都会创建一个averager的实例 25 results[key] = yield from averager() # grouper发送的每个值都会让yield from处理,把产出的值绑定给resuluts[key] 26 27 28 # 客户端代码,即调用方 29 def main(data): # main函数是客户端代码 30 results = {} 31 for key, values in data.items(): 32 group = grouper(results, key) # group是调用grouper的生成器 33 next(group) # 预激group协程 34 for value in values: 35 group.send(value) # 把各个value的值传递给grouper,通过grouper传入averager中term 36 group.send(None) # 所有值传递结束以后,终止averager 37 #print(results) # 如果要调试,去掉注释 38 report(results) 39 40 #输出报告 41 def report(results): 42 for key, result in sorted(results.items()): 43 group, unit = key.split(';') 44 print('{:2} {:5} averaging {:.2f}{}'.format( 45 result.count, group, result.average, unit)) 46 47 48 data = { 49 'girls;kg': 50 [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5], 51 'girls;m': 52 [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43], 53 'boys;kg': 54 [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3], 55 'boys;m': 56 [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46], 57 } 58 59 if __name__ == '__main__': 60 main(data)
以上代码执行的结果为:
9 boys averaging 40.42kg 9 boys averaging 1.39m 10 girls averaging 42.04kg 10 girls averaging 1.43m
下面简要说明上面🌰的运作方式,还会说明把 main 函数中调用group.send(None) 那一行代码(带有“重要!”注释的那一行)去掉会发生什么事。
- 外层 for 循环每次迭代会新建一个 grouper 实例,赋值给 group变量;group 是委派生成器。
- 调用 next(group),预激委派生成器 grouper,此时进入 whileTrue 循环,调用子生成器 averager 后,在 yield from 表达式处暂停。
- 内层 for 循环调用 group.send(value),直接把值传给子生成器averager。同时,当前的 grouper 实例(group)在 yieldfrom 表达式处暂停。
- 内层循环结束后,group 实例依旧在 yield from 表达式处暂停,因此,grouper 函数定义体中为 results[key] 赋值的语句还没有执行。
- 如果外层 for 循环的末尾没有 group.send(None),那么averager 子生成器永远不会终止,委派生成器 group 永远不会再次激活,因此永远不会为 results[key] 赋值。
- 外层 for 循环重新迭代时会新建一个 grouper 实例,然后绑定到group 变量上。前一个 grouper 实例(以及它创建的尚未终止的averager 子生成器实例)被垃圾回收程序回收。
yield from的意义
把迭代器当作生成器使用,相当于把子生成器的定义体内联在yield from 表达式中。此外,子生成器可以执行 return 语句,返回一个值,而返回的值会成为 yield from 表达式的值
批准后的 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 异常。
使用案例:使用协程做离散事件仿真
协程能自然地表述很多算法,例如仿真、游戏、异步 I/O,以及其他事件驱动型编程形式或协作式多任务。
离散事件仿真简介
离散事件仿真(Discrete Event Simulation,DES)是一种把系统建模成一系列事件的仿真类型。在离散事件仿真中,仿真“钟”向前推进的量不是固定的,而是直接推进到下一个事件模型的模拟时间。假如我们抽象模拟出租车的运营过程,其中一个事件是乘客上车,下一个事件则是乘客下车。不管乘客坐了 5 分钟还是 50 分钟,一旦乘客下车,仿真钟就会更新,指向此次运营的结束时间。使用离散事件仿真可以在不到一秒钟的时间内模拟一年的出租车运营过程。这与连续仿真不同,连续仿真的仿真钟以固定的量(通常很小)不断向前推进。
显然,回合制游戏就是离散事件仿真的例子:游戏的状态只在玩家操作时变化,而且一旦玩家决定下一步怎么走了,仿真钟就会冻结。而实时游戏则是连续仿真,仿真钟一直在运行,游戏的状态在一秒钟之内更新很多次,因此反应慢的玩家特别吃亏。
这两种仿真类型都能使用多线程或在单个线程中使用面向事件的编程技术(例如事件循环驱动的回调或协程)实现。可以说,为了实现连续仿真,在多个线程中处理实时并行的操作更自然。而协程恰好为实现离散事件仿真提供了合理的抽象。SimPy 是一个实现离散事件仿真的Python 包,通过一个协程表示离散事件仿真系统中的各个进程。