我们这里说的协程,是指通过生成器实现的。事实上,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