如果 Python 书籍有一定的指导作用,那么(协程就是)文档最匮乏、最鲜为人知的 Python 特性,因此表面上看是最无用的特性。
字典为动词 “to yield” 给出了两个释义:产出和让步。对于 Python 生成器中的 yield
来说,这两个含义都成立。 yield item
这行代码会产出一个值,提供给 next(...)
的调用方;此外,还会做出让步,让 next(...)
调用方继续工作,直到需要使用另一个值时再调用 next()
。调用方会从生成器中拉取值。
从句法上看,协程和生成器类似,都是定义体中包含 yield
关键字的函数。可是,在协程中,yield 通常出现在表达式的右边(例如:data = yield
),可以产出值,也可以不产出——如果 yield
关键字后面没有表达式,那么生成器产出 None
。协程可能从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(data)
方法,而不是 next(...)
函数。通常,调用方会把值推送给协程。
yield
关键字甚至可以不接受或者传出数据。不管数据怎么流动,yield
都是一种流程控制工具,使用它可以实现协作式多任务:协程可以把控制器让步给中心调度程序,从而激活其他的协程。
一个简单的协程
1 | # simple_coroutine.py |
输出:
1 | <generator object simple_coroutine at 0x0000017DB0FE11A8> |
协程可以身处四个状态之一。当前状态可以使用 inspect.getgeneratorstate(...)
函数确定,该函数会返回下述字符串中的一个:
状态 | 说明 |
---|---|
GEN_CREATED | 等待开始执行 |
GEN_RUNNING | 解释器正在执行 |
GEN_SUSPENDED | 在 yield 表达式处暂停 |
GEN_CLOSED | 执行结束 |
因为 send
方法的参数会成为暂停的 yield
表达式的值,所以,仅当协程处于暂停状态时才能调用 send
方法。所以,在开始发送数据前,需要预激协程(一般是 send(None)
或者 next(my_coro)
),让协程从 'GEN_CREATED'
状态转到 'GEN_SUSPENDED'
。
示例:使用协程计算移动平均值
1 | # example_averager.py |
averager()
函数中的无限循环表明,只要调用方不断把值发给这个协程,他就会一直接收值,然后生成结果。仅当调用方在协程上调用 .close()
方法,或者没有对协程的引用而被垃圾回收程序回收时,这个协程才会终止。
通过装饰器预激协程
1 | # example_decorator.py |
使用 yield from
句法调用协程时,会自动预激,因此与示例中的 @coroutine
等装饰器不兼容。Python 标准库里的 `@asyncio.coroutine装饰器不会预激协程,因此能兼容
yield from` 句法。
终止协程和异常处理
1 | from example_decorator import averager |
发送的值不是数字时协程内出现异常,协程会停止,并且异常冒泡到调用方。如果试图重新激活协程,会抛出 StopIteration 异常。
可以在协程的 yield
语句周围加上 try/except
块来捕获可能出现的异常,或者给异常附加信息,然后向上冒泡。
限于篇幅,异常处理不多讲,在 asyncio 中会详细讲异常处理。
不论何时,只要出现了异常,协程的状态都会变成 'GEN_CLOSED'
,所以不能再重新激活协程。
让协程返回值
1 | from collections import namedtuple |
运行结果:
1 | Traceback (most recent call last): |
通过判断变量 term
是否为 None
来决定是否终止协程,终止协程时返回一个 Result
具名元组类对象,附加在 StopIteration
异常的信息中。
关于协程的介绍就到这里了,下一篇会讲 yield from
语句的定义与使用。