我们这里说的协程,是指通过生成器实现的。事实上,Python 3.5 引入了新的关键字 async 和 await 来定义协程,与我们这里的协程实现不同。当然,因为 Python 3.5 才引入 async 和 await 关键字,所有 Python 3.5 之前和 Python 2 都是通过下面的方法实现协程的。如果要了解 async 和 await,可以看看这个:PEP 492 – Coroutines with async and await syntax

在协程之前,yield 是作为声明(statement)使用的。为了实现协程,yield 被重新定义为表达式。一个简单的协程例子就是:

>>> def test():
...    i = 0
...    while True:
...        result = yield i
...        print('Got: {}'.format(result))
...        i += 1

可以看到,因为 yield 可作表达式,于是 result 获得了 yield 表达式的值,但是这个值是哪里来的呢?根据 PEP 255 Simple Generator,yield 后面的表达式的值, 即 i 将被返回给 next() 方法的调用者。所以,result 不可能等于 i。事实上,result 的值来自外界。

PEP 342 Coroutines via Enhanced Generator 为实现协程定义了三个新方法,其中一个是 send(),签名如下:

g.send(value)

其中 g 表示一个生成器,下面类似。send() 方法接受一个参数 value,之后该 value 将被发送到生成器内部,作为 yield 表达式的值。一个例子如下:

>>> t = test()
>>> t
<generator object test at 0x7fd44d30eb48>
>>> t.send(None)
0
>>> t.send('I am the value')
Got: I am the value
1
>>> t.send('I am the value of result')
Got: I am the value of result
2

事实上,send() 的作用可以描述为:恢复生成器的执行,并向其中发送一个值,该值成为 yield 表达式的值。即,调用 send(None) 的效果等价于调用 next() 。而生成器 yield 回来的值则将成为 send() 方法的返回值,在这里就是 i。如果生成器已经退出或者停止,那么调用 send() 就会得到 StopIteration 异常。同时,如果生成器中引发了未捕获的异常,那么该异常会继续传递给 send() 的调用者。

对于一个刚创建的生成器对象,这时还没有执行函数体,如果用一个不为 None 的参数调用 send() 方法,那么就会得到 TypeError

>>> t=test()
>>> t.send(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator

所以对于刚创建的生成器对象,要启动生成器的执行,应该使用 send(None) 或者 next() 方法。

再来看看 yield 作为表达式的限制,除非 yield 表达式出现在等号右边的最顶层(top-level),那么一律都需要加括号,如下面的例子都是允许的:

x = yield 42
x = yield
x = 12 + (yield 42)
x = 12 + (yield)
foo(yield 42)
foo(yield)

而下面的例子则是错误的:

x = 12 + yield 42
x = 12 + yield
foo(yield 42, 12)
foo(yield, 12)

当然 yield 后可以不加表达式,这样的话,调用 next() 或者 send(value) 将得到 None。

PEP 342 还定义两个其他方法,分别是 throw() and close()。throw() 方法签名如下:

g.throw(type, value=None, traceback=None)

g.throw(type, value, traceback) 会使得生成器 g 在当前暂停的位置引发特定的异常(即,传入的异常类型),等价于在当前暂停位置执行下面语句:

raise type,value,traceback

一个简单的例子如下:

>>> t=test()
>>> next(t)
0
>>> next(t)
Got: None
1
>>> t.throw(ValueError)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in test
ValueError
>>> next(t)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

如上面,对生成器调用了 throw() 方法,传入了 ValueError 异常,那么生成器就会在当前暂停位置引发 ValueError 异常。因为生成器内部没有捕获这个异常,于是该异常继续传递给 throw() 方法的调用者。同时,因为生成器内部产生了异常,导致了它的停止。那么下次再使用 next() 或者 send() 方法,就会得到StopIteration 异常。当然,如果生成器早已停止,那么调用 throw 异常将会传递回相同的异常。但是,如果生成器内部捕获了传入的异常,并且 yield 回了一个新的值,那么该值将成为 throw() 方法的返回值。

最后一个方法是 close,伪代码定义如下:

def close(self):
    try:
        self.throw(GeneratorExit)
    except (GeneratorExit,StopIteration):
        pass
    else:
        raise RuntimeError("generator ignored GeneratorExit")

基本上,close 通过引发一个 GeneratorExit 异常来停止生成器。如果生成器已经退出或者停止,就会引发 StopIteration 异常,这时 close() 方法就会捕获它进行处理。而如果这时生成器仍然继续 yield 回一个新的值,那么就会引发RuntimeError 异常。

总结: Python 为了实现协程,为生成器定义三个新方法,其中 send() 可以向暂停的生成器发送值,throw() 方法可以发送异常,而 close() 则可以停止生成器。 但是,协程在概念上来说,是不同于生成器的!在这里,Python 只是利用生成器的特性实现了协程,本质上还是一个生成器,但是我们可以用它完成协程的功能。 要得到一个真正意义上的协程,我们可以使用 Python 3.5 中的新关键字 async和 await

参考: PEP 255 – Simple Generators PEP 342 – Coroutines via Enhanced Generators

 

Previous post: 理解 Python 的生成器

Next post: 理解 Python 的 yield from