If Python books are any guide, [coroutines are] the most poorly documented, obscure, and apparently useless feature of Python. — David Beazley Python author
如果Python这本书是本指南的话,而协程就是最缺少文档、而又鲜为人知,似乎很无用的Python的一个功能。-Python作者David Beazley
We find two main senses for the verb “to yield” in dictionaries: to produce or to give way. Both senses apply in Python when we use the yield keyword in a generator. A line such as yield item produces a value that is received by the caller of next(...), and it also gives way, suspending the execution of the generator so that the caller may proceed until it’s ready to consume another value by invoking next() again. The caller pulls values from the generator.
我们在字典中发现动词“生成”的两个主要含义:产生或者让渡。这两个含义都可以在我们需要在生成器中使用yield关键字时在Python中运用。
A coroutine is syntactically like a generator: just a function with the yield keyword in its body. However, in a coroutine, yield usually appears on the right side of an expres‐ sion (e.g., datum = yield), and it may or may not produce a value—if there is no expression after the yield keyword, the generator yields None. The coroutine may re‐ ceive data from the caller, which uses .send(datum) instead of next(...) to feed the coroutine. Usually, the caller pushes values into the coroutine.
协程在语法上类似于生成器:无非是一个在语句主体中包含yield关键字的函数。不过,在协程中,yield通常出现在表达式的右边(比如,datum = yield),而且如果在yield关键字之后没有表达式,它可以产生值也可以不产生值——生成器生成None。协从调用者接受数据,调用者使用send(datum)而不是next(...)反馈给协程。通常,调用者会把值放进协程中。
It is even possible that no data goes in or out through the yield keyword. Regardless of the flow of data, yield is a control flow device that can be used to implement cooperative multitasking: each coroutine yields control to a central scheduler so that other corou‐ tines can be activated.
When you start thinking of yield primarily in terms of control flow, you have the mindset to understand coroutines.
当你开始
Python coroutines are the product of a series of enhancements to the humble generator functions we’ve seen so far in the book. Following the evolution of coroutines in Python helps understand their features in stages of increasing functionality and complexity.
Python协程
After a brief overview of how generators were enable to act as a coroutine, we jump to the core of the chapter. Then we’ll see:
在一个生成器如何能够充当协程的简短概要之后,我们走进了本章的核心。接下来我们可以看到:
-
The behavior and states of a generator operating as a coroutine
-
Priming a coroutine automatically with a decorator
-
How the caller can control a coroutine through the .close() and .throw(...) methods of the generator object
-
How coroutines can return values upon termination
-
Usage and semantics of the new yield from syntax
-
A use case: coroutines for managing concurrent activities in a simulation
-
当作协程操作的生成器行为和状态
-
使用装饰器自动地启用一个协程
-
调用者如何能够通过生成器对象的.close()和.throw(...)方法控制协程
-
协程如何能在终止之前返回值
-
用例:在模拟环境中用协程管理并发活动
The infrastructure for coroutines appeared in PEP 342 — Coroutines via Enhanced Generators, implemented in Python 2.5 (2006): since then, the yield keyword can be used in an expression, and the .send(value) method was added to the generator API. Using .send(...), the caller of the generator can post data that then becomes the value of the yield expression inside the generator function. This allows a generator to be used as a coroutine: a procedure that collaborates with the caller, yielding and receiving values from the caller.
协程的基本架构出现在PEP 342中——由生成器加强过的协程,在Python2.5(2006年)中得以实现:从那时候起,yield关键字便可用在表达式中,同时.send(value)方法被加入到了生成器API。
In addition to .send(...), PEP 342 also added .throw(...) and .close() methods that respectively allow the caller to throw an exception to be handled inside the generator, and to terminate it. These features are covered in the next section and in “Coroutine Termination and Exception Handling” on page 471.
除了.send(...)之外,PEP 342增加了.throw(...)和.close()方法,已分别允许调用者在生成器内部抛出一个异常,并终止程序。这些功能将在下一节和471页上的“协程终止与异常处理”讲到。
The latest evolutionary step for coroutines came with PEP 380 - Syntax for Delegating to a Subgenerator, implemented in Python 3.3 (2012). PEP 380 made two syntax changes to generator functions, to make them more useful as coroutines:
协程的最新演化路径随着PEP 380到来——即,在Python3.3(2012年)实现的分派子生成器语法。PEP 380对生成器做了两个语法上的改变,以便
-
A generator can now return a value; previously, providing a value to the return statement inside a generator raised a SyntaxError.
-
The yield from syntax enables complex generators to be refactored into smaller, nested generators while avoiding a lot of boilerplate code previously required for a generator to delegate to subgenerators.
-
现在生成器可以返回一个值;
-
yield from语法能够让复杂的生成器重构到更小,
These latest changes will be addressed in “Returning a Value from a Coroutine” on page 475 and “Using yield from” on page 477.
这些最新的改变声明在475页中的“从协程中返回一个值”,以及477页上的“使用yield from”。
Let’s follow the established tradition of Fluent Python and start with some very basic facts and examples, then move into increasingly mind-bending features.
我们遵照Fluent Python所建立的传统,从一些非常基本的样例开始,然后一步步接近那些令人难以理解的特性。
Example 16-1 illustrates the behavior of a coroutine.
例子16-1 阐明了协程的行为。
Example 16-1. Simplest possible demonstration of coroutine in action
例子16-1。
>>> def simple_coroutine(): # 1
... print('-> coroutine started')
... x = yield # 2
... print('-> coroutine received:', x)
...
>>> my_coro = simple_coroutine()
>>> my_coro # 3
<generator object simple_coroutine at 0x100c2be10>
>>> next(my_coro) # 4
-> coroutine started
>>> my_coro.send(42) # 5
-> coroutine received: 42
Traceback (most recent call last): # 6
...
StopIteration
-
A coroutine is defined as a generator function: with yield in its body. 协程像生成器函数一样被定义:在函数体内使用yield。
-
yield is used in an expression; when the coroutine is designed just to receive data from the client it yields None—this is implicit because there is no expression to the right of the yield keyword. yield被用在表达式中;
-
As usual with generators, you call the function to get a generator object back.
-
The first call is next(...) because the generator hasn’t started so it’s not waiting in a yield and we can’t send it any data initially.
-
This call makes the yield in the coroutine body evaluate to 42; now the coroutine resumes and runs until the next yield or termination.
-
In this case, control flows off the end of the coroutine body, which prompts the generator machinery to raise StopIteration, as usual.
A coroutine can be in one of four states. You can determine the current state using the inspect.getgeneratorstate(...) function, which returns one of these strings:
协程可以是四种状态中的一种。
'GEN_CREATED' Waiting to start execution. 'GEN_RUNNING' Currently being executed by the interpreter.[1]
'GEN_CREATED' 为开始执行做等待 'GEN_RUNNING'
[1. You’ll only see this state in a multithreaded application—or if the generator object calls getgenerator state on itself, which is not useful.]
【注释1. 】
'GEN_SUSPENDED' Currently suspended at a yield expression. 'GEN_CLOSED' Execution has completed.
Because the argument to the send method will become the value of the pending yield expression, it follows that you can only make a call like my_coro.send(42) if the coro‐ utine is currently suspended. But that’s not the case if the coroutine has never been activated—when its state is 'GEN_CREATED'. That’s why the first activation of a coroutine is always done with next(my_coro)—you can also call my_coro.send(None), and the effect is the same.
因为参数
If you create a coroutine object and immediately try to send it a value that is not None, this is what happens:
>>> 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
Note the error message: it’s quite clear.
The initial call next(my_coro) is often described as “priming” the coroutine (i.e., ad‐ vancing it to the first yield to make it ready for use as a live coroutine).
To get a better feel for the behavior of a coroutine, an example that yields more than once is useful. See Example 16-2.
为了更好的感受协程的行为,有一个yield不止一次的例子就显得很有用了。参见例子16-2.
Example 16-2. A coroutine that yields twice
例子16-2. 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) # 1
'GEN_CREATED'
>>> next(my_coro2) # 2
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2) # 3
'GEN_SUSPENDED'
>>> my_coro2.send(28) # 4
-> Received: b = 28
42
>>> my_coro2.send(99) # 5
-> Received: c = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module> StopIteration
>>> getgeneratorstate(my_coro2) # 6
'GEN_CLOSED'
- inspect.getgeneratorstate reports
GEN_CREATED
(i.e., the coroutine has not started). inspect.getgeneratorstate报告了协程的状态为GEN_CREATED
(例如,在协程还未启动时)。 - Advance coroutine to first yield, printing
-> Started: a = 14
message then yielding value of a and suspending to wait for value to be assigned to b. 促使协程第一次yield,即打印了消息-> Started: a = 14
,然后生成了a的值,并挂起以等待将该值赋值给b。 - getgeneratorstate reports
GEN_SUSPENDED
(i.e., the coroutine is paused at a yield expression). getgeneratorstate报告协程的状态为GEN_SUSPENDED
(例如,协程暂停在了yield表达式) - Send number 28 to suspended coroutine; the yield expression evaluates to 28 and that number is bound to b. The
-> Received: b = 28
message is displayed, the value of a + b is yielded (42), and the coroutine is suspended waiting for the value to be assigned to c. 发送数字28到挂起的协程;yield表达式求值为28,并把这个数值绑定到b。随着消息-> Received: b = 28
显示,a+b的yield的值也生成了,然后协程挂起,并等待将该值赋给c。 - Send number 99 to suspended coroutine; the yield expression evaluates to 99 the number is bound to c. The
-> Received: c = 99 message
is displayed, then the coroutine terminates, causing the generator object to raise StopIteration. 发送数字99到已挂起的协程;yield表达式将所求之值99绑定到了c上。随着-> Received: c = 99 message
的显示,协程终止,并使生成器对象抛出StopIteration。 - getgeneratorstate reports
GEN_CLOSED
(i.e., the coroutine execution has completed). getgeneratorstate报告协程的状态为GEN_CLOSED
(例如,在协程执行完成后。)
It’s crucial to understand that the execution of the coroutine is suspended exactly at the yield keyword. As mentioned before, in an assignment statement, the code to the right of the = is evaluated before the actual assignment happens. This means that in a line like b = yield a, the value of b will only be set when the coroutine is activated later by the client code. It takes some effort to get used to this fact, but understanding it is essential to make sense of the use of yield in asynchronous programming, as we’ll see later.
理解协程的执行完全就是挂起在了yield关键字是件极为重要的事情。咱们之前也提到过,在赋值语句中等于号右边的代码,在实际赋值之前就被求值了。这就意味着,在类似b = yield a
的代码行中,b的值,将只会在协程通过用户执行代码激活后才赋值。要了解这种状况需要付出一点时间,但你要明白,这也是理解yield在异步编程中最基本的使用。
Execution of the simple_coro2 coroutine can be split in three phases, as shown in Figure 16-1:
如图表16-1所示,协程simple_coro2的执行可以分割到三个阶段:
next(my_coro2)
prints first message and runs to yield a, yielding number 14.next(my_coro2)
打印第一条消息,并运行yield a,生成数字14.my_coro2.send(28)
assigns 28 to b, prints second message, and runs toyield a + b
, yielding number 42.my_coro2.send(28)
把28赋给b,答应第二条消息,然后运行yield a + b
,生成数字42.my_coro2.send(99)
assigns 99 to c, prints third message, and the coroutine ter‐ minates.my_coro2.send(99)
将99付给了c,打印第三条消息,然后协程终止。
Figure 16-1. Three phases in the execution of the simple_coro2 coroutine (note that each phase ends in a yield expression, and the next phase starts in the very same line, when the value of the yield expression is assigned to a variable)
图表16-1. 协程simple_coro2
Now let’s consider a slightly more involved coroutine example.
现在,我们来思考一个稍微深入一些的协程示例。
While discussing closures in Chapter 7, we studied objects to compute a running aver‐ age: Example 7-8 shows a plain class and Example 7-14 presents a higher-order function producing a closure to keep the total and count variables across invocations. Example 16-3 shows how to do the same with a coroutine.[2]
在第七章讨论闭包时,我们学习了计算运行平均时间的主题:例子7-8显示的一个普通类,例子7-14展示了一个产生必包并保存
[注释2]2. This example is inspired by a snippet from Jacob Holm in the Python-ideas list, message titled “Yield-From: Finalization guarantees.” Some variations appear later in the thread, and Holm further explains his thinking in message 003912.
【注释2】
Example 16-3. coroaverager0.py: code for a running average coroutine
例子16-3. coroaverager0.py:运行平均数的协程代码。
def averager():
total = 0.0
count = 0
average = None
while True: #1
term = yield average #2
total += term
count += 1
average = total/count
- This infinite loop means this coroutine will keep on accepting values and producing results as long as the caller sends them. This coroutine will only terminate when the caller calls .close() on it, or when it’s garbage collected because there are no more references to it. 这个无限循环表明,只要调用者向协程发送内容,协程会一直接受值并输出结果。该协程只会在调用这对它使用close时终止,或者是在被执行垃圾回收之后,因为再也没有对这个协程的引用了。
- The yield statement here is used to suspend the coroutine, produce a result to the caller, and—later—to get a value sent by the caller to the coroutine, which resumes its infinite loop. 此处的yield语句用来挂起协程,给调用者生产一个结果,然后等待调用者发送到协程的值,然后协程重新进入无限循环。
The advantage of using a coroutine is that total and count can be simple local variables: no instance attributes or closures are needed to keep the context between calls. Example 16-4 are doctests to show the averager coroutine in operation.
使用协程的好处在于total和count可以简化为本地变量:不在需要实例属性或者闭包来保存多次调用之间的上下文。例子16-4,
Example 16-4. coroaverager0.py: doctest for the running average coroutine in Example 16-3
例子16-4. coroaverager0.py:对例子16-3中的运行平均协程的doctest。
>>> coro_avg = averager()
>>> next(coro_avg)
>>> coro_avg.send(10)
10.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(5)
15.0
- Create the coroutine object. 创建协程对象。
- Prime it by calling next. 让协程为next调用做好准备。
- Now we are in business: each call to .send(...) yields the current average. 现在我们已经就绪:每次对于send的调用都生成当前的值。
In the doctest (Example 16-4), the call next(coro_avg) makes the coroutine advance to the yield, yielding the initial value for average, which is None, so it does not appear on the console. At this point, the coroutine is suspended at the yield, waiting for a value to be sent. The line coro_avg.send(10)
provides that value, causing the coroutine to activate, assigning it to term, updating the total, count, and average variables, and then starting another iteration in the while loop, which yields the average and waits for another term.
在doctest中(例子16-4),调用next()让协程促使yield,为average生成初始值,而值为None,所以不会出现在终端上。就在此刻,协程挂起于yield处,等待着值发送给它。行coro_avg.send(10)
提供了这个值,致使协程激活,将该值赋给term,更新total,count,和变量average,然后在while循环中启动另外一个迭代,
The attentive reader may be anxious to know how the execution of an averager instance (e.g., coro_avg) may be terminated, because its body is an infinite loop. We’ll cover that in “Coroutine Termination and Exception Handling” on page 471.
敏感的读者或许对于如何知道averager实例的终止感到发怒,因为协程的程序体是一个无限循环。我们会在471页中的“协程的终止和异常处理”讲到。
But before discussing coroutine termination, let’s talk about getting them started. Pri‐ ming a coroutine before use is a necessary but easy-to-forget chore. To avoid it, a special decorator can be applied to the coroutine. One such decorator is presented next.
但是在我们讨论协程终止之前,我们来说说如何启动它。
You can’t do much with a coroutine without priming it: we must always remember to call next(my_coro) before my_coro.send(x). To make coroutine usage more convenient, a priming decorator is sometimes used. The coroutine decorator in Example 16-5 is an example.[3]
你无法使用一个没有准备好的协程:我们必须总得记得在执行my_coro.send(x)之前调用next(my_coro)。为了让协程使用起来更方便,
[注释3]There are several similar decorators published on the Web. This one is adapted from the ActiveState recipe Pipeline made of coroutines by Chaobin Tang, who in turn credits David Beazley.
【注释3】有多个类似的装饰器公布在网上。
Example 16-5. coroutil.py: decorator for priming coroutine
例子16-5.
from functools import wraps
def coroutine(func):
"""Decorator: primes `func` by advancing to first `yield`"""
@wraps(func)
def primer(*args,**kwargs): #1
gen = func(*args,**kwargs) #2
next(gen) #3
return gen #4
return primer
- The decorated generator function is replaced by this primer function which, when invoked, returns the primed generator. 装饰生成器的函数辈这个基本的函数替换,在其被调用时,返回原始装饰器。
- Call the decorated function to get a generator object. 调用被装饰的函数以获取生成器对象。
- Prime the generator. 原始装饰器。
- Return it. 返回生成器。
Example 16-6 shows the @coroutine decorator in use. Contrast with Example 16-3.
例子16-6 展示了装饰器@coroutine的使用。对标例子16-3.
Example 16-6. coroaverager1.py: doctest and code for a running average coroutine us‐ ing the @coroutine decorator from Example 16-5
例子16-6. coroaverager1.py:
"""
A coroutine to compute a running average
>>> 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():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total/count
- Call averager(), creating a generator object that is primed inside the primer function of the coroutine decorator.
- getgeneratorstate reports GEN_SUSPENDED, meaning that the coroutine is ready to receive a value.
- You can immediately start sending values to coro_avg: that’s the point of the decorator.
- Import the coroutine decorator.
- Apply it to the averager function.
- The body of the function is exactly the same as Example 16-3.
Several frameworks provide special decorators designed to work with coroutines. Not all of them actually prime the coroutine—some provide other services, such as hooking it to an event loop. One example from the Tornado asynchronous networking library is the tornado.gen decorator.
The yield from syntax we’ll see in “Using yield from” on page 477 automatically primes the coroutine called by it, making it incompatible with decorators such as @coroutine from Example 16-5. The asyncio.coroutine decorator from the Python 3.4 standard library is designed to work with yield from so it does not prime the coroutine. We’ll cover it in Chapter 18. We’ll now focus on essential features of coroutines: the methods used to terminate and throw exceptions into them.
An unhandled exception within a coroutine propagates to the caller of the next or send that triggered it. Example 16-7 is an example using the decorated averager coroutine from Example 16-6.
Example 16-7. How an unhandled exception kills a coroutine
>>> from coroaverager1 import averager
>>> coro_avg = averager()
>>> coro_avg.send(40) #1
40.0
>>> coro_avg.send(50)
45.0
>>> coro_avg.send('spam') #2
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +=: 'float' and 'str'
>>> coro_avg.send(60) #3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
- Using the @coroutine decorated averager we can immediately start sending values.
- Sending a nonnumeric value causes an exception inside the coroutine.
- Because the exception was not handled in the coroutine, it terminated. Any attempt to reactivate it will raise StopIteration.
The cause of the error was the sending of a value 'spam' that could not be added to the total variable in the coroutine.
Example 16-7 suggests one way of terminating coroutines: you can use send with some sentinel value that tells the coroutine to exit. Constant built-in singletons like None and Ellipsis are convenient sentinel values. Ellipsis has the advantage of being quite unusual in data streams. Another sentinel value I’ve seen used is StopIteration—the class itself, not an instance of it (and not raising it). In other words, using it like: my_co ro.send(StopIteration).
Since Python 2.5, generator objects have two methods that allow the client to explicitly send exceptions into the coroutine—throw and close:
-
generator.throw(exc_type[, exc_value[, traceback]]) Causes the yield expression where the generator was paused to raise the exception given. If the exception is handled by the generator, flow advances to the next yield, and the value yielded becomes the value of the generator.throw call. If the exception is not handled by the generator, it propagates to the context of the caller.
-
generator.close() Causes the yield expression where the generator was paused to raise a Generator Exit exception. No error is reported to the caller if the generator does not handle that exception or raises StopIteration—usually by running to completion. When receiving a GeneratorExit, the generator must not yield a value, otherwise a Run timeError is raised. If any other exception is raised by the generator, it propagates to the caller.
###Note The official documentation of the generator object methods is buried deep in The Python Language Reference, (see 6.2.9.1. Generator-iterator methods).
Let’s see how close and throw control a coroutine. Example 16-8 lists the demo_exc_han dling function used in the following examples.
Example 16-8. coro_exc_demo.py: test code for studying exception handling in a corou‐ tine
class DemoException(Exception):
"""An exception type for the demonstration."""
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
- Special handling for DemoException.
- If no exception, display received value.
- This line will never be executed.
The last line in Example 16-8 is unreachable because the infinite loop can only be aborted with an unhandled exception, and that terminates the coroutine immediately.
Normal operation of demo_exc_handling is shown in Example 16-9.
Example 16-9. Activating and closing demo_exc_handling without an exception
>>> 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'
If the DemoException is thrown into the coroutine, it’s handled and the demo_exc_han dling coroutine continues, as in Example 16-10.
Example 16-10. Throwing DemoException into demo_exc_handling does not break it
>>> 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'
On the other hand, if an unhandled exception is thrown into the coroutine, it stops— its state becomes 'GEN_CLOSED'. Example 16-11 demonstrates it.
Example 16-11. Coroutine terminates if it can’t handle an exception thrown into it
>>> 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'
If it’s necessary that some cleanup code is run no matter how the coroutine ends, you need to wrap the relevant part of the coroutine body in a try/finally block, as in Example 16-12.
Example 16-12. coro_finally_demo.py: use of try/finally to perform actions on corou‐ tine termination
class DemoException(Exception):
"""An exception type for the demonstration."""
def demo_finally():
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')
One of the main reasons why the yield from construct was added to Python 3.3 has to do with throwing exceptions into nested coroutines. The other reason was to enable coroutines to return values more conveniently. Read on to see how.
Example 16-13 shows a variation of the averager coroutine that returns a result. For didactic reasons, it does not yield the running average with each activation. This is to emphasize that some coroutines do not yield anything interesting, but are designed to return a value at the end, often the result of some accumulation.
The result returned by averager in Example 16-13 is a namedtuple with the number of terms averaged (count) and the average. I could have returned just the average value, but returning a tuple exposes another interesting piece of data that was accumulated: the count of terms.
Example 16-13. coroaverager2.py: code for an averager coroutine that returns a result
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
- In order to return a value, a coroutine must terminate normally; this is why this version of averager has a condition to break out of its accumulating loop.
- Return a namedtuple with the count and average. Before Python 3.3, it was a syntax error to return a value in a generator function.
To see how this new averager works, we can drive it from the console, as in Example 16-14.
Example 16-14. coroaverager2.py: doctest showing the behavior of 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):
...
StopIteration: Result(count=3, average=15.5)
- This version does not yield values.
- Sending None terminates the loop, causing the coroutine to end by returning the result. As usual, the generator object raises StopIteration. The value attribute of the exception carries the value returned.
Note that the value of the return expression is smuggled to the caller as an attribute of the StopIteration exception. This is a bit of a hack, but it preserves the existing be‐ havior of generator objects: raising StopIteration when exhausted.
Example 16-15 shows how to retrieve the value returned by the coroutine.
Example 16-15. Catching StopIteration lets us get the value returned by 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)
This roundabout way of getting the return value from a coroutine makes more sense when we realize it was defined as part of PEP 380, and the yield from construct handles it automatically by catching StopIteration internally. This is analogous to the use of StopIteration in for loops: the exception is handled by the loop machinery in a way that is transparent to the user. In the case of yield from, the interpreter not only con‐ sumes the StopIteration, but its value attribute becomes the value of the yield from expression itself. Unfortunately we can’t test this interactively in the console, because it’s a syntax error to use yield from—or yield, for that matter—outside of a function.[4]
[Note4]There is an iPython extension called ipython-yf that enables evaluating yield from directly in the iPython console. It’s used to test asynchronous code and works with asyncio. It was submitted as a patch to Python 3.5 but was not accepted. See Issue #22412: Towards an asyncio-enabled command line in the Python bug tracker.
The next section has an example where the averager coroutine is used with yield from to produce a result, as intended in PEP 380. So let’s tackle yield from.
The first thing to know about yield from is that it is a completely new language con‐ struct. It does so much more than yield that the reuse of that keyword is arguably misleading. Similar constructs in other languages are called await, and that is a much better name because it conveys a crucial point: when a generator gen calls yield from subgen(), the subgen takes over and will yield values to the caller of gen; the caller will in effect drive subgen directly. Meanwhile gen will be blocked, waiting until subgen terminates.[5]
[Note5]As I write this, there is an open PEP proposing the addition of await and async keywords: PEP 492 — Coroutines with async and await syntax.
We’ve seen in Chapter 14 that yield from can be used as a shortcut to yield in a for loop. For example, this:
>>> def gen():
... for c in 'AB':
... yield c
... for i in range(1, 3):
... yield i
...
>>> list(gen())
['A', 'B', 1, 2]
Can be written as:
>>> def gen():
... yield from 'AB'
... yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]
When we first mentioned yield from in “New Syntax in Python 3.3: yield from” on page 433, the code from Example 16-16 demonstrates a practical use for it.[6]
[Note6]Example 16-16 is a didactic example only. The itertools module already provides an optimized chain function written in C.
Example 16-16. Chaining iterables with yield from
>>> def chain(*iterables):
... for it in iterables:
... yield from it
...
>>> s = 'ABC'
>>> t = tuple(range(3))
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]
A slightly more complicated—but more useful—example of yield from is in “Recipe 4.14. Flattening a Nested Sequence” in Beazley and Jones’s Python Cookbook, 3E (source code available on GitHub).
The first thing the yield from x expression does with the x object is to call iter(x) to obtain an iterator from it. This means that x can be any iterable.
However, if replacing nested for loops yielding values was the only contribution of yield from, this language addition wouldn’t have had a good chance of being accepted. The real nature of yield from cannot be demonstrated with simple iterables; it requires the mind-expanding use of nested generators. That’s why PEP 380, which introduced yield from, is titled “Syntax for Delegating to a Subgenerator.”
The main feature of yield from is to open a bidirectional channel from the outermost caller to the innermost subgenerator, so that values can be sent and yielded back and forth directly from them, and exceptions can be thrown all the way in without adding a lot of exception handling boilerplate code in the intermediate coroutines. This is what enables coroutine delegation in a way that was not possible before.
The use of yield from requires a nontrivial arrangement of code. To talk about the required moving parts, PEP 380 uses some terms in a very specific way:
- delegating generator
The generator function that contains the
yield from <iterable>
expression. - subgenerator
The generator obtained from the
<iterable>
part of the yield from expression. This is the “subgenerator” mentioned in the title of PEP 380: “Syntax for Delegating to a Subgenerator.” - caller PEP 380 uses the term “caller” to refer to the client code that calls the delegating generator. Depending on context, I use “client” instead of “caller,” to distinguish from the delegating generator, which is also a “caller” (it calls the subgenerator).
###Note PEP 380 often uses the word “iterator” to refer to the subgenera‐ tor. That’s confusing because the delegating generator is also an iterator. So I prefer to use the term subgenerator, in line with the title of the PEP—“Syntax for Delegating to a Subgenerator.” How‐ ever, the subgenerator can be a simple iterator implementing on‐ ly
__next__
, and yield from can handle that too, although it was created to support generators implementing__next__,
send, close, and throw.
Example 16-17 provides more context to see yield from at work, and Figure 16-2 identifies the relevant parts of the example.[7]
[Note7]The picture in Figure 16-2 was inspired by a diagram by Paul Sokolovsky.
Figure 16-2. While the delegating generator is suspended at yield from, the caller sends data directly to the subgenerator, which yields data back to the caller. The delegating generator resumes when the subgenerator returns and the interpreter raises StopItera‐ tion with the returned value attached.
The coroaverager3.py script reads a dict with weights and heights from girls and boys in an imaginary seventh grade class. For example, the key 'boys;m' maps to the heights of 9 boys, in meters; 'girls;kg' are the weights of 10 girls in kilograms. The script feeds the data for each group into the averager coroutine we’ve seen before, and pro‐ duces a report like this one:
$ python3 coroaverager3.py
9 boys averaging 40.42kg
9 boys averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m
The code in Example 16-17 is certainly not the most straightforward solution to the problem, but it serves to show yield from in action. This example is inspired by the one given in What’s New in Python 3.3.
Example 16-17. coroaverager3.py: using yield from to drive averager and report statistics
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) #10
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)
- Same
averager
coroutine from Example 16-13. Here it is the subgenerator. - Each value sent by the client code in
main
will be bound toterm
here. - The crucial terminating condition. Without it, a
yield from
calling this coroutine will block forever. - The returned
Result
will be the value of theyield from
expression ingrouper
. grouper
is the delegating generator.- Each iteration in this loop creates a new instance of averager; each is a generator object operating as a coroutine.
- Whenever
grouper
is sent a value, it’s piped into theaverager
instance by theyield from
.grouper
will be suspended here as long as the averager instance is consuming values sent by the client. When an averager instance runs to the end, the value it returns is bound toresults[key]
. The while loop then proceeds to create another averager instance to consume more values. - main is the client code, or “caller” in PEP 380 parlance. This is the function that drives everything.
- group is a generator object resulting from calling grouper with the results dict to collect the results, and a particular key. It will operate as a coroutine.
- Prime the coroutine.
- Send each value into the grouper. That value ends up in the
term = yield
line ofaverager
;grouper
never has a chance to see it. - Sending None into
grouper
causes the currentaverager
instance to terminate, and allows grouper to run again, which creates anotheraverager
for the next group of values.
The last callout in Example 16-17 with the comment "important!" highlights a crucial line of code: group.send(None), which terminates one averager and starts the next. If you comment out that line, the script produces no output. Uncommenting the print(re sults) line near the end of main reveals that the results dict ends up empty.
####Note If you want to figure out for yourself why no results are collec‐ ted, it will be a great way to exercise your understanding of how yield from works. The code for coroaverager3.py is in the Flu‐ ent Python code repository. The explanation is next.
Here is an overview of how Example 16-17 works, explaining what would happen if we omitted the call group.send(None) marked “important!” in main:
- Each iteration of the outer for loop creates a new grouper instance named group; this is the delegating generator.
- The call next(group) primes the grouper delegating generator, which enters its while True loop and suspends at the yield from, after calling the subgenerator averager.
- The inner for loop calls group.send(value); this feeds the subgenerator averag er directly. Meanwhile, the current group instance of grouper is suspended at the yield from.
- When the inner for loop ends, the group instance is still suspended at the yield from, so the assignment to results[key] in the body of grouper has not happened yet.
- Without the last group.send(None) in the outer for loop, the averager subgen‐ erator never terminates, the delegating generator group is never reactivated, and the assignment to results[key] never happens.
- When execution loops back to the top of the outer for loop, a new grouper instance is created and bound to group. The previous grouper instance is garbage collected (together with its own unfinished averager subgenerator instance).
####Catious The key takeaway from this experiment is: if a subgenerator nev‐ er terminates, the delegating generator will be suspended forever at the yield from. This will not prevent your program from mak‐ ing progress because the yield from (like the simple yield) trans‐ fers control to the client code (i.e., the caller of the delegating generator). But it does mean that some task will be left unfinished.
Example 16-17 demonstrates the simplest arrangement of yield from, with only one delegating generator and one subgenerator. Because the delegating generator works as a pipe, you can connect any number of them in a pipeline: one delegating generator uses yield from to call a subgenerator, which itself is a delegating generator calling another subgenerator with yield from, and so on. Eventually this chain must end in a simple generator that uses just yield, but it may also end in any iterable object, as in Example 16-16.
Every yield from chain must be driven by a client that calls next(...)
or .send(...)
on the outermost delegating generator. This call may be implicit, such as a for loop.
Now let’s review the formal description of the yield from construct, as presented in PEP 380.
While developing PEP 380, Greg Ewing—the author—was questioned about the com‐ plexity of the proposed semantics. One of his answers was “For humans, almost all the important information is contained in one paragraph near the top.” He then quoted part of the draft of PEP 380 which at the time read as follows:
“When the iterator is another generator, the effect is the same as if the body of the sub‐ generator were inlined at the point of the yield from expression. Furthermore, the subgenerator is allowed to execute a return statement with a value, and that value be‐ comes the value of the yield from expression.”[8]
[Note8]Message to Python-Dev: “PEP 380 (yield from a subgenerator) comments” (March 21, 2009).
Those soothing words are no longer part of the PEP—because they don’t cover all the corner cases. But they are OK as a first approximation.
The approved version of PEP 380 explains the behavior of yield from in six points in the Proposal section. I reproduce them almost exactly here, except that I replaced every occurrence of the ambiguous word “iterator” with “subgenerator” and added a few clarifications. Example 16-17 illustrates these four points:
- Any values that the subgenerator yields are passed directly to the caller of the del‐ egating generator (i.e., the client code).
- Any values sent to the delegating generator using send() are passed directly to the subgenerator. If the sent value is None, the subgenerator’s next() method is called. If the sent value is not None, the subgenerator’s send() method is called. If the call raises StopIteration, the delegating generator is resumed. Any other ex‐ ception is propagated to the delegating generator.
- return expr in a generator (or subgenerator) causes StopIteration(expr) to be raised upon exit from the generator.
- The value of the yield from expression is the first argument to the StopItera tion exception raised by the subgenerator when it terminates.
The other two features of yield from have to do with exceptions and termination:
- Exceptions other than GeneratorExit thrown into the delegating generator are passed to the throw() method of the subgenerator. If the call raises StopItera tion, the delegating generator is resumed. Any other exception is propagated to the delegating generator.
- If a GeneratorExit exception is thrown into the delegating generator, or the close() method of the delegating generator is called, then the close() method of the subgenerator is called if it has one. If this call results in an exception, it is propagated to the delegating generator. Otherwise, GeneratorExit is raised in the delegating generator.
The detailed semantics of yield from are subtle, especially the points dealing with exceptions. Greg Ewing did a great job putting them to words in English in PEP 380.
Ewing also documented the behavior of yield from using pseudocode (with Python syntax). I personally found it useful to spend some time studying the pseudocode in PEP 380. However, the pseudocode is 40 lines long and not so easy to grasp at first.
A good way to approach that pseudocode is to simplify it to handle only the most basic and common use case of yield from
.
Consider that yield from appears in a delegating generator. The client code drives delegating generator, which drives the subgenerator. So, to simplify the logic involved, let’s pretend the client doesn’t ever call .throw(...) or .close() on the delegating gen‐ erator. Let’s also pretend the subgenerator never raises an exception until it terminates, when StopIteration is raised by the interpreter.
Example 16-17 is a script where those simplifying assumptions hold. In fact, in much real-life code, the delegating generator is expected to run to completion. So let’s see how yield from works in this happier, simpler world.
Take a look at Example 16-18, which is an expansion of this single statement, in the body of the delegating generator:
RESULT = yield from EXPR
Try to follow the logic in Example 16-18.
Example 16-18. Simplified pseudocode equivalent to the statement RESULT = yield from EXPR in the delegating generator (this covers the simplest case: .throw(...) and .close() are not supported; the only exception handled is 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 #8
- The
EXPR
can be any iterable, because iter() is applied to get an iterator _i (this is the subgenerator). - The subgenerator is primed; the result is stored to be the first yielded value _y.
- If StopIteration was raised, extract the value attribute from the exception and assign it to _r: this is the RESULT in the simplest case.
- While this loop is running, the delegating generator is blocked, operating just as a channel between the caller and the subgenerator.
- Yield the current item yielded from the subgenerator; wait for a value _s sent by the caller. Note that this is the only yield in this listing.
- Try to advance the subgenerator, forwarding the _s sent by the caller.
- If the subgenerator raised StopIteration, get the value, assign to _r, and exit the loop, resuming the delegating generator.
- _r is the RESULT: the value of the whole yield from expression.
In this simplified pseudocode, I preserved the variable names used in the pseudocode published in PEP 380. The variables are:
_i (iterator)
The subgenerator_y (yielded)
A value yielded from the subgenerator_r (result)
The eventual result (i.e., the value of the yield from expression when the subgen‐ erator ends)_s (sent)
A value sent by the caller to the delegating generator, which is forwarded to the subgenerator_e (exception)
An exception (always an instance of StopIteration in this simplified pseudocode)
Besides not handling .throw(...) and .close(), the simplified pseudocode always uses .send(...) to forward next() or .send(...) calls by the client to the subgenerator. Don’t worry about these fine distinctions on a first reading. As mentioned, Example 16-17 would run perfectly well if the yield from did only what is shown in the simplified pseudocode in Example 16-18.
But the reality is more complicated, because of the need to handle .throw(...) and .close() calls from the client, which must be passed into the subgenerator. Also, the subgenerator may be a plain iterator that does not support .throw(...) or .close(), so this must be handled by the yield from logic. If the subgenerator does implement those methods, inside the subgenerator both methods cause exceptions to be raised, which must be handled by the yield from machinery as well. The subgenerator may also throw exceptions of its own, unprovoked by the caller, and this must also be dealt with in the yield from implementation. Finally, as an optimization, if the caller calls next(...) or .send(None), both are forwarded as a next(...) call on the subgenerator; only if the caller sends a non-None value, the .send(...) method of the subgenerator is used.
For your convenience, following is the complete pseudocode of the yield from expan‐ sion from PEP 380, syntax-highlighted and annotated. Example 16-19 was copied ver‐ batim; only the callout numbers were added by me.
Again, the code shown in Example 16-19 is an expansion of this single statement, in the body of the delegating generator:
RESULT = yield from EXPR
Example 16-19. Pseudocode equivalent to the statement RESULT = yield from EXPR in the delegating generator
_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
- The EXPR can be any iterable, because iter() is applied to get an iterator _i (this is the subgenerator).
- The subgenerator is primed; the result is stored to be the first yielded value _y.
- If StopIteration was raised, extract the value attribute from the exception and assign it to _r: this is the RESULT in the simplest case.
- While this loop is running, the delegating generator is blocked, operating just as a channel between the caller and the subgenerator.
- Yield the current item yielded from the subgenerator; wait for a value _s sent by the caller. This is the only yield in this listing.
- This deals with closing the delegating generator and the subgenerator. Because the subgenerator can be any iterator, it may not have a close method.
- This deals with exceptions thrown in by the caller using .throw(...). Again, the subgenerator may be an iterator with no throw method to be called—in which case the exception is raised in the delegating generator.
- If the subgenerator has a throw method, call it with the exception passed from the caller. The subgenerator may handle the exception (and the loop continues); it may raise StopIteration (the _r result is extracted from it, and the loop ends); or it may raise the same or another exception, which is not handled here and propagates to the delegating generator.
- If no exception was received when yielding...
- Try to advance the subgenerator...
- Call next on the subgenerator if the last value received from the caller was None, otherwise call send.
- If the subgenerator raised StopIteration, get the value, assign to _r, and exit the loop, resuming the delegating generator.
- _r is the RESULT: the value of the whole yield from expression.
Most of the logic of the yield from pseudocode is implemented in six try/except blocks nested up to four levels deep, so it’s a bit hard to read. The only other control flow keywords used are one while, one if, and one yield. Find the while, the yield, the next(...), and the .send(...) calls: they will help you get an idea of how the whole structure works.
Right at the top of Example 16-19, one important detail revealed by the pseudocode is that the subgenerator is primed (second callout in Example 16-19).[9] This means that auto-priming decorators such as that in “Decorators for Coroutine Priming” on page 469 are incompatible with yield from.
[9]9. In a message to Python-ideas on April 5, 2009, Nick Coghlan questioned whether the implicit priming done by yield from was a good idea.
In the same message I quoted in the opening of this section, Greg Ewing has this to say about the pseudocode expansion of yield from:
You’re not meant to learn about it by reading the expansion—that’s only there to pin down all the details for language lawyers.
Focusing on the details of the pseudocode expansion may not be helpful—depending on your learning style. Studying real code that uses yield from is certainly more prof‐ itable than poring over the pseudocode of its implementation. However, almost all the yield from examples I’ve seen are tied to asynchronous programming with the asyn cio module, so they depend on an active event loop to work. We’ll see yield from numerous times in Chapter 18. There are a few links in “Further Reading” on page 500 to interesting code using yield from without an event loop.
We’ll now move on to a classic example of coroutine usage: programming simulations. This example does not showcase yield from, but it does reveal how coroutines are used to manage concurrent activities on a single thread.
Coroutines are a natural way of expressing many algorithms, such as simulations, games, asynchronous I/O, and other forms of event-driven programming or co-operative mul‐ titasking.[10] — Guido van Rossum and Phillip J. Eby PEP 342—Coroutines via Enhanced Generators
[10]10. Opening sentence of the “Motivation” section in PEP 342.
In this section, I will describe a very simple simulation implemented using just corou‐ tines and standard library objects. Simulation is a classic application of coroutines in the computer science literature. Simula, the first OO language, introduced the concept of coroutines precisely to support simulations.
The motivation for the following simulation example is not aca‐ demic. Coroutines are the fundamental building block of the asyncio package. A simulation shows how to implement concur‐ rent activities using coroutines instead of threads—and this will greatly help when we tackle asyncio with in Chapter 18.
Before going into the example, a word about simulations.
A discrete event simulation (DES) is a type of simulation where a system is modeled as a sequence of events. In a DES, the simulation “clock” does not advance by fixed incre‐ ments, but advances directly to the simulated time of the next modeled event. For ex‐ ample, if we are simulating the operation of a taxi cab from a high-level perspective, one event is picking up a passenger, the next is dropping the passenger off. It doesn’t matter if a trip takes 5 or 50 minutes: when the drop off event happens, the clock is updated to the end time of the trip in a single operation. In a DES, we can simulate a year of cab trips in less than a second. This is in contrast to a continuous simulation where the clock advances continuously by a fixed—and usually small—increment.
Intuitively, turn-based games are examples of discrete event simulations: the state of the game only changes when a player moves, and while a player is deciding the next move, the simulation clock is frozen. Real-time games, on the other hand, are continuous simulations where the simulation clock is running all the time, the state of the game is updated many times per second, and slow players are at a real disadvantage.
Both types of simulations can be written with multiple threads or a single thread using event-oriented programming techniques such as callbacks or coroutines driven by an event loop. It’s arguably more natural to implement a continuous simulation using threads to account for actions happening in parallel in real time. On the other hand, coroutines offer exactly the right abstraction for writing a DES. SimPy[11] is a DES package for Python that uses one coroutine to represent each process in the simulation.
[11]See the official documentation for Simpy—not to be confused with the well-known but unrelated SymPy, a library for symbolic mathematics.
In the field of simulation, the term process refers to the activities of an entity in the model, and not to an OS process. A simulation process may be implemented as an OS process, but usually a thread or a coroutine is used for that purpose.
If you are interested in simulations, SimPy is well worth studying. However, in this section, I will describe a very simple DES implemented using only standard library features. My goal is to help you develop an intuition about programming concurrent actions with coroutines. Understanding the next section will require careful study, but the reward will come as insights on how libraries such as asyncio, Twisted, and Tornado can manage many concurrent activities using a single thread of execution.
In our simulation program, taxi_sim.py, a number of taxi cabs are created. Each will make a fixed number of trips and then go home. A taxi leaves the garage and starts “prowling”—looking for a passenger. This lasts until a passenger is picked up, and a trip starts. When the passenger is dropped off, the taxi goes back to prowling.
The time elapsed during prowls and trips is generated using an exponential distribution. For a cleaner display, times are in whole minutes, but the simulation would work as well using float intervals.12 Each change of state in each cab is reported as an event.
Figure 16-3 shows a sample run of the program.
Figure 16-3. Sample run of taxi_sim.py with three taxis. The -s 3 argument sets the random generator seed so program runs can be reproduced for debugging and demon‐ stration. Colored arrows highlight taxi trips.
The most important thing to note in Figure 16-3 is the interleaving of the trips by the three taxis. I manually added the arrows to make it easier to see the taxi trips: each arrow starts when a passenger is picked up and ends when the passenger is dropped off. In‐ tuitively, this demonstrates how coroutines can be used for managing concurrent ac‐ tivities.
Other things to note about Figure 16-3:
• Each taxi leaves the garage 5 minutes after the other. • It took 2 minutes for taxi 0 to pick up the first passenger at time=2; 3 minutes for taxi 1 (time=8), and 5 minutes for taxi 2 (time=15). • The cabbie in taxi 0 only makes two trips (purple arrows): the first starts at time=2 and ends at time=18; the second starts at time=28 and ends at time=65—the longest trip in this simulation run. • Taxi 1 makes four trips (green arrows) then goes home at time=110. • Taxi 2 makes six trips (red arrows) then goes home at time=109. His last trip lasts only one minute, starting at time=97.[13] • While taxi 1 is making her first trip, starting at time=8, taxi 2 leaves the garage at time=10 and completes two trips (short red arrows). • In this sample run, all scheduled events completed in the default simulation time of 180 minutes; last event was at time=110.
[13]I was the passenger. I realized I forgot my wallet.
The simulation may also end with pending events. When that happens, the final message reads like this:
*** end of simulation time: 3 events pending ***
The full listing of taxi_sim.py is at Example A-6. In this chapter, we’ll show only the parts that are relevant to our study of coroutines. The really important functions are only two: taxi_process (a coroutine), and the Simulator.run method where the main loop of the simulation is executed.
Example 16-20 shows the code for taxi_process. This coroutine uses two objects de‐ fined elsewhere: the compute_delay function, which returns a time interval in minutes, and the Event class, a namedtuple defined like this:
Event = collections.namedtuple('Event', 'time proc action')
In an Event instance, time is the simulation time when the event will occur, proc is the identifier of the taxi process instance, and action is a string describing the activity.
Let’s review taxi_process play by play in Example 16-20.
Example 16-20. taxi_sim.py: taxi_process coroutine that implements the activities of each taxi
def taxi_process(ident, trips, start_time=0):
"""Yield to simulator issuing event at each state change""" time = yield Event(start_time, ident, 'leave garage')
for i in range(trips):
time = yield Event(time, ident, 'pick up passenger') time = yield Event(time, ident, 'drop off passenger')
yield Event(time, ident, 'going home') # end of taxi process