1. PEP 342 简介
Python2.2 引入了 yield
关键字实现的生成器函数。大约 五年后,Python2.5 实现了 PEP 342 — “Coroutines via Enhanced Generators”。这个提案为生成器对象添加了额外的方法和功能,其中最值得关注的是 .send()
方法。
与 __next__()
方法不同,send()
方法使得生成器前进到下一个 yield
语句。不过, send()
方法还允许使用生成器的客户把数据发送给自己,即不管传递给 send()
方法什么参数,那个参数都会成为生成器函数定义体中对应的 yield
表达式的值。也就是说, send()
方法允许在客户代码和生成器之间双向交换数据。而 __next__
方法只允许客户从生成器中获取数据。
这是一项重要的改进,甚至改变了生成器的本性:像这样使用的话,生成器就变身为协程。但有几点需要注意:
- 生成器用于生成供迭代的数据。
- 协程是数据的消费者。
- 协程与迭代无关。
- 虽然在协程中会使用
yield
产出值,但这与迭代无关。
2. 动力
协程是表达许多算法的一种自然方式,例如模拟、游戏、异步 I/O 和其他形式的事件驱动编程或协作多任务处理。Python的生成器函数几乎已经是协程(但还不完全是),因为它们允许暂停执行来生成一个值,但不提供在执行恢复时接收传入的值或异常。它们还不允许在 try/finally
块的 try
部分暂停执行,因此,使得中止的协程很难在自身之后进行清理。
此外,生成器在执行其他函数时无法让出控制权,除非这些函数本身表示为生成器且外部生成器被写入 yield
以响应内部生成器生成的值。这使异步通信等相对简单的用例的实现变得复杂,因为调用任何函数都需要生成器阻塞(无法让出控制权),或者必须在每个需要的函数调用周围添加大量样板循环代码。
然而,如果有可能在生成器挂起时将值或异常传递到生成器中,那么一个简单的协程调度器或 trampoline 函数将允许协程在不阻塞的情况下彼此调用,这对于异步应用程序来说是一个巨大的好处。然后,这些应用程序可以编写协程来执行非阻塞套接字 I/O,方法是将控制权交给 I/O 调度程序,直到数据发送或可用为止。同时,执行 I/O 的代码会简单地做如下事情:
data = (yield nonblocking_read(my_socket, nbytes))
为了暂停执行,直到 nonblocking_read()
协程产生一个值。
换句话说,对于语言和 generator-iterator
类型实现的一些相对较小的增强,使得 Python 将能够支持执行异步操作,而不需要把整个应用程序写成一系列回调,并且不需要使用资源密集型线程来处理需要数百甚至数千个协作多任务伪线程的程序。因此,这些增强将给标准 Python 带来 “Stackless Python fork” 的许多好处,而不需要对 CPython 核心或其API 进行任何重大修改。此外,这些增强应该很容易被任何已经支持生成器的 Python 实现(例如 Jython)实现。
3. 规范概要
通过向生成器-迭代器类型添加一些简单的方法,并进行两个较小的语法调整,Python开发人员将能够使用生成器函数来实现协程和其他形式的协作多任务处理。这些方法和调整是:
- 将
yield
重定义为一个表达式,而不是一个语句。当前的yield
语句会成为一个丢弃其值的yield
表达式。当正常的next()
调用恢复生成器时,yield
表达式的值为 None。 - 为生成器-迭代器添加一个新的
send()
方法,该方法恢复生成器并发送一个值,该值将成为当前yield
表达式的结果。send()
方法返回生成器生成的下一个值,或者在生成器退出时没有生成另一个值时引发 StopIteration。 - 为生成器-迭代器添加一个新的
throw()
方法,该方法在生成器暂停时引发异常,并返回生成器生成的下一个值,如果生成器退出时没有生成另一个值,则引发 StopIteration。(如果生成器没有捕获传入的异常,或者引发不同的异常,则该异常将传播到调用者。) - 为生成器-迭代器添加
close()
方法,该方法在生成器暂停时引发 GeneratorExit。如果生成器随后引发StopIteration(通过正常退出,或者由于已经关闭)或 GeneratorExit(通过不捕获异常),close()
返回给它的调用者。如果生成器生成一个值,则会引发 RuntimeError。如果生成器引发任何其他异常,则将其传播给调用者。如果生成器已经由于异常而退出或者正常退出,close()
将不执行任何操作。 - 添加支持以确保对成器迭代器进行垃圾回收时调用
close()
。 - 允许在
try/finally
块中使用yield
,因为垃圾回收或显式的close()
调用现在允许执行finally
子句。
4. 规范:将值发送到生成器中
4.1 新的生成器方法:send(value)
提出了一种新的生成迭代器方法 send()
。它只接受一个参数,即应该发送到生成器的值。调用 send(None)
与调用生成器的 next()
方法完全相同。使用任何其他值调用 send()
都是相同的,只是生成器的当前 yield
表达式生成的值不同。
因为生成器-迭代器从生成器函数体的顶部开始执行,所以在刚刚创建生成器时没有 yield
表达式来接收值。因此,当生成器迭代器刚刚启动时,禁止使用非 None 参数调用 send()
,如果发生这种情况(可能是由于某种逻辑错误),就会引发 TypeError。因此,在与协程通信之前,必须首先调用 next()
或 send(None)
来将其执行推进到第一个 yield
表达式。
与 next()
方法一样,send()
方法返回生成器-迭代器生成的下一个值,如果生成器正常退出,或者已经退出,则引发StopIteration 。如果生成器引发未捕获的异常,则将其传播给 send()
的调用者。
4.2 新语法:yield 表达式
yield
语句将被允许用在赋值表达式的右边:在这种情况下,它被称为 yield 表达式。除非使用非 None 参数调用 send()
,否则这个 yield
表达式的值为 None;见下文。
除非出现在顶级表达式的赋值语句的右边,否则必须始终在 yield
表达式中加上圆括号。所以:
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-statement
或 yield-expression
现在是合法的。这是有意义的:当 next()
调用中的信息流被反转时,应该可以在不传递显式值的情况下产出值(yield
当然等于 yield None
)。
当调用 send(value)
时,恢复的 yield
表达式将返回传入的值。当调用 next(
)时,恢复的 yield
表达式将返回None。如果 yield
表达式是一个 yield
语句,则忽略该返回值,类似于忽略作为语句使用的函数调用返回的值。
实际上,yield
表达式就像一个倒置的函数调用;yield
的参数实际上是从当前执行的函数返回的,yield
的返回值是通过 send()
传入的参数。
注意:yield
的语法扩展使它的使用非常类似于 Ruby。这是故意的。请注意,在 Python中,块使用 send(EXPR)
将一个值传递给生成器,而不是返回 EXPR,而在生成器和块之间传递控制的底层机制是完全不同的。Python 中的块不会编译成 thunks
;相反,yield
会暂停生成器帧的执行。一些边缘的用例的工作方式不同;在Python中,您不能保存该块供以后使用,也不能测试是否有块。
5. 规范:异常和清理
调用生成器函数会返回一个生成器对象。下面的内容中,g
总是表示一个生成器对象。
5.1 新语法:允许 yield 出现在 try-finally 内部
生成器函数的语法经过扩展,允许在 try-finally
语句中使用 yield
语句。
5.2 新的生成器方法:throw(type, value=None, traceback=None)
g.throw(type, value, traceback)
导致在生成器 g
当前挂起的位置抛出指定的异常(即在 yield
语句中,或者在尚未调用 next()
方法时,函数体的开始处)。如果生成器捕获异常并生成另一个值,那就是 g.throw()
的返回值。如果它没有捕获异常,throw()
似乎会引发传递给它的相同异常(它会失败)。如果生成器引发另一个异常(包括返回时产生的 StopIteration),则 throw()
调用将引发该异常。总之,throw()
的行为类似于 next()
或 send()
,只是它会在挂起点引发异常。如果生成器已经处于关闭状态,throw()
则只会引发传给它的异常,而不执行任何生成器代码。
引发异常的效果与在挂起点执行下面的语句完全相同:
raise type, value, traceback
type 参数不能为 None,type 和 value 必须兼容。如果 value 不是 type 的实例,则使用 value 创建一个新的异常实例,遵循 raise
语句用于创建异常实例的相同规则。如果提供 traceback 参数,则必须是一个有效的 Python 回溯对象,否则会发生 TypeError。
注意:选择 throw()
方法的名称有几个原因。raise
是一个关键字,因此不能用作方法名。与 raise
(它会立即从当前执行点引发异常)不同,throw()
首先恢复生成器,然后才引发异常。单词 “throw” 暗示将异常放在另一个位置,并且已经与其他语言中的异常相关联。
5.3 新的标准异常:GeneratorExit
定义了一个新的异常 GeneratorExit,该异常继承自 Exception。生成器应该通过重新抛出这个异常(或者只是不捕获它)或引发 StopInteration
来处理这个 GeneratorExit 异常。
5.4 新的生成器方法:close()
g.close()
通过下面的伪代码定义:
def close(self):
try:
self.throw(GeneratorExit)
except (GeneratorExit, StopIteration):
pass
else:
raise RuntimeError("generator ignored GeneratorExit")
# Other exceptions are not caught
5.5 新的生成器方法:__del__()
是 g.close()
的一个包装器。这将在生成器对象被垃圾回收时调用(在 CPython 中,当它的引用计数为零时)。如果close()
引发异常,则会将异常的回溯打印到 sys.stderr
并进一步忽略;它不会传播回触发垃圾回收的位置。这与处理类实例上的 __del__()
方法中的异常是一致的。
如果生成器对象参与了一个循环,则可能不会调用 g.__del__()
。这是 CPython 当前垃圾回收器的行为。这种限制的原因是,GC 代码需要在任意点上中断一个循环,以便回收它,从那时起,不应该允许 Python 代码看到构成循环的对象,因为它们可能处于无效状态。挂在循环上的对象不受此限制。
注意,在实践中不太可能看到生成器对象参与循环。但是,将生成器对象存储在全局变量中会通过生成器帧的 f_globals
指针创建一个循环。创建一个循环的另一种方法是将生成器对象的引用存储在一个将要作为参数传递给生成器的数据结构中(例如,如果一个对象有一个方法,它是一个生成器,并且保持对该方法创建的正在运行的迭代器的引用)。考虑到典型的生成器使用模式,这两种情况都不太可能发生。
此外,在此 PEP 的 CPython 实现中,每当生成器的执行由于错误或正常退出而终止时,应该释放生成器使用的 frame
对象。这将确保不能恢复的生成器不会成为不可回收的引用循环的一部分。这允许其他代码可能在 try/finally
中使用close()
或与块一起使用(每个 PEP 343 ),以确保给定的生成器已正确终结。
6. 示例
6.1 消费者装饰器
一个简单的 消费者 装饰器,使生成器在第一次调用时能自动推进到它的第一个 yield
点:
def consumer(func):
def wrapper(*args,**kw):
gen = func(*args, **kw)
gen.next()
return gen
wrapper.__name__ = func.__name__
wrapper.__dict__ = func.__dict__
wrapper.__doc__ = func.__doc__
return wrapper
6.2 使用消费者装饰器创建反向生成器
使用 消费者 装饰器创建一个 反向生成器 的示例,该生成器接收图像并创建缩略图页面,然后将其发送给另一个消费者。这样的功能可以链接在一起,形成高效的消费者处理管道,每个消费者都可以有复杂的内部状态:
@consumer
def thumbnail_pager(pagesize, thumbsize, destination):
while True:
page = new_image(pagesize)
rows, columns = pagesize / thumbsize
pending = False
try:
for row in xrange(rows):
for column in xrange(columns):
thumb = create_thumbnail((yield), thumbsize)
page.write(
thumb, col*thumbsize.x, row*thumbsize.y )
pending = True
except GeneratorExit:
# close() was called, so flush any pending output
if pending:
destination.send(page)
# then close the downstream consumer, and exit
destination.close()
return
else:
# we finished a page full of thumbnails, so send it
# downstream and keep on looping
destination.send(page)
@consumer
def jpeg_writer(dirname):
fileno = 1
while True:
filename = os.path.join(dirname,"page%04d.jpg" % fileno)
write_jpeg((yield), filename)
fileno += 1
# Put them together to make a function that makes thumbnail
# pages from a list of images and other parameters.
#
def write_thumbnails(pagesize, thumbsize, images, output_dir):
pipeline = thumbnail_pager(
pagesize, thumbsize, jpeg_writer(output_dir)
)
for image in images:
pipeline.send(image)
pipeline.close()
6.3 一个简单的协例程调度器
一个简单的协程调度器或 trampoline,使协程可以通过产出它们希望调用的协程来调用其他协程。协程产生的任何非生成器值都会返回给调用产生该值的那个协程的协程。类似地,如果协程引发异常,则将异常传播到其调用者。实际上,这个例子模拟了在无堆栈Python中使用的简单微线程,只要您使用一个yield表达式来调用那些可能会阻塞的例程即可。这只是一个非常简单的例子,而且可以使用复杂得多的调度程序。(例如,用于Python的现有 [GTasklet框架](http://www.gnome.org/~gjc/ GTasklet /gtasklets.html) 和 peak.events 框架 已经实现了类似的调度功能,但目前必须使用笨拙的变通方法来解决无法将值或异常传递到生成器的问题。
import collections
class Trampoline:
"""Manage communications between coroutines"""
running = False
def __init__(self):
self.queue = collections.deque()
def add(self, coroutine):
"""Request that a coroutine be executed"""
self.schedule(coroutine)
def run(self):
result = None
self.running = True
try:
while self.running and self.queue:
func = self.queue.popleft()
result = func()
return result
finally:
self.running = False
def stop(self):
self.running = False
def schedule(self, coroutine, stack=(), val=None, *exc):
def resume():
value = val
try:
if exc:
value = coroutine.throw(value,*exc)
else:
value = coroutine.send(value)
except:
if stack:
# send the error back to the "caller"
self.schedule(
stack[0], stack[1], *sys.exc_info()
)
else:
# Nothing left in this pseudothread to
# handle it, let it propagate to the
# run loop
raise
if isinstance(value, types.GeneratorType):
# Yielded to a specific coroutine, push the
# current one on the stack, and call the new
# one with no args
self.schedule(value, (coroutine,stack))
elif stack:
# Yielded a result, pop the stack and send the
# value to the caller
self.schedule(stack[0], stack[1], value)
# else: this pseudothread has ended
self.queue.append(resume)
6.4 一个简单的 echo 服务器
一个简单的echo服务器,并使用蹦床运行它的代码(假设存在 nonblocking_read
、nonblocking_write
和其他 I/O 协程,例如,在连接关闭时引发 ConnectionLost):
# coroutine function that echos data back on a connected
# socket
#
def echo_handler(sock):
while True:
try:
data = yield nonblocking_read(sock)
yield nonblocking_write(sock, data)
except ConnectionLost:
pass # exit normally if connection lost
# coroutine function that listens for connections on a
# socket, and then launches a service "handler" coroutine
# to service the connection
#
def listen_on(trampoline, sock, handler):
while True:
# get the next incoming connection
connected_socket = yield nonblocking_accept(sock)
# start another coroutine to handle the connection
trampoline.add( handler(connected_socket) )
# Create a scheduler to manage all our coroutines
t = Trampoline()
# Create a coroutine instance to run the echo_handler on
# incoming connections
#
server = listen_on(
t, listening_socket("localhost","echo"), echo_handler
)
# Add the coroutine to the scheduler
t.add(server)
# loop forever, accepting connections and servicing them
# "in parallel"
#
t.run()