PEP 342 – 通过增强型生成器实现协程
- 作者:
- Guido van Rossum, Phillip J. Eby
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2005年5月10日
- Python 版本:
- 2.5
- 发布历史:
引言
本 PEP 提出对生成器的 API 和语法进行一些增强,使其可用作简单的协程。它基本上结合了这两个 PEP 中的思想,如果本 PEP 被接受,这两个 PEP 可能被视为多余。
动机
协程是表达许多算法的自然方式,例如模拟、游戏、异步 I/O 以及其他形式的事件驱动编程或协作式多任务处理。Python 的生成器函数几乎就是协程——但又不完全是——因为它们允许暂停执行以产生一个值,但当执行恢复时,不提供传入值或异常。它们也不允许在 try/finally 块的 try 部分内暂停执行,因此使得中止的协程难以自行清理。
此外,生成器不能在其他函数执行时交出控制权,除非这些函数本身也表示为生成器,并且外部生成器被编写为响应内部生成器生成的值而交出控制权。这使得即使是相对简单的用例(如异步通信)的实现也变得复杂,因为调用任何函数要么需要生成器 阻塞(即无法交出控制权),要么必须在每个需要的函数调用周围添加大量的样板循环代码。
然而,如果能够在生成器暂停时将值或异常 传入 生成器,那么一个简单的协程调度器或 trampoline 函数 将允许协程在不阻塞的情况下相互 调用——这对于异步应用程序来说是一个巨大的福音。然后,这些应用程序可以编写协程通过将控制权交给 I/O 调度器来执行非阻塞套接字 I/O,直到数据被发送或可用。同时,执行 I/O 的代码只需这样做:
data = (yield nonblocking_read(my_socket, nbytes))
以便暂停执行,直到 nonblocking_read() 协程产生一个值。
换句话说,通过对语言和生成器迭代器类型实现进行一些相对较小的增强,Python 将能够支持执行异步操作,而无需将整个应用程序编写为一系列回调,也无需为需要数百甚至数千个协作多任务伪线程的程序使用资源密集型线程。因此,这些增强将使标准 Python 获得 Stackless Python 分支的许多好处,而无需对 CPython 核心或其 API 进行任何重大修改。此外,这些增强应该很容易被任何已经支持生成器的 Python 实现(如 Jython)实现。
规范摘要
通过向生成器迭代器类型添加几个简单的方法,并进行两个小的语法调整,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子句执行。
一个实现所有这些更改的原型补丁(针对当前的 Python CVS HEAD)可在 SourceForge 补丁 #1223381 (https://bugs.python.org/issue1223381) 获取。
规范:向生成器发送值
新的生成器方法:send(value)
提议为生成器迭代器添加一个新方法,名为 send()。它只接受一个参数,该参数是将要 发送 到生成器的值。调用 send(None) 完全等同于调用生成器的 next() 方法。调用 send() 传入任何其他值也是相同的,只是生成器当前 yield 表达式产生的值会不同。
由于生成器迭代器在生成器函数体的顶部开始执行,所以在生成器刚创建时没有 yield 表达式来接收值。因此,在生成器迭代器刚启动时,禁止调用带有非 None 参数的 send(),如果发生这种情况(可能是由于某种逻辑错误),则会引发 TypeError。因此,在与协程通信之前,您必须首先调用 next() 或 send(None) 以将其执行推进到第一个 yield 表达式。
与 next() 方法一样,send() 方法返回生成器迭代器产生的下一个值,如果生成器正常退出或已经退出,则引发 StopIteration。如果生成器引发未捕获的异常,则该异常会传播给 send() 的调用者。
新语法:Yield 表达式
yield 语句将被允许用在赋值语句的右侧;在这种情况下,它被称为 yield 表达式。除非 send() 被调用时带有非 None 参数,否则此 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 12, 42 当前合法性的启发。)
请注意,现在没有表达式的 yield 语句或 yield 表达式是合法的。这很有意义:当 next() 调用中的信息流反转时,应该可以在不传递显式值的情况下进行 yield(yield 当然等同于 yield None)。
当调用 send(value) 时,它恢复的 yield 表达式将返回传入的值。当调用 next() 时,恢复的 yield 表达式将返回 None。如果 yield 表达式是 yield 语句,则此返回值将被忽略,类似于忽略用作语句的函数调用返回的值。
实际上,yield 表达式就像一个反向函数调用;yield 的参数实际上是从当前执行的函数中返回(yield)的,而 yield 的 返回值 是通过 send() 传入的参数。
注意:对 yield 的语法扩展使其用法与 Ruby 中的用法非常相似。这是有意为之的。请注意,在 Python 中,块使用 send(EXPR) 而不是 return EXPR 将值传递给生成器,并且控制在生成器和块之间传递的底层机制是完全不同的。Python 中的块不会编译成 thunk;相反,yield 暂停了生成器帧的执行。一些边界情况的工作方式不同;在 Python 中,您无法保存块以供以后使用,也无法测试是否存在块。(XXX - 关于块的这部分现在看起来格格不入,也许 Guido 可以编辑以澄清。)
规范:异常和清理
设生成器对象是调用生成器函数产生的迭代器。在下文中,g 始终指代一个生成器对象。
新语法:yield 允许在 try-finally 内部使用
生成器函数的语法已扩展,允许在 try-finally 语句内部使用 yield 语句。
新的生成器方法: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 traceback 对象,否则会发生 TypeError。
注意:throw() 方法的名称是出于多种原因选择的。Raise 是一个关键字,因此不能用作方法名。与 raise(它立即从当前执行点引发异常)不同,throw() 首先恢复生成器,然后才引发异常。单词 throw 暗示将异常放置在另一个位置,并且在其他语言中已经与异常相关联。
考虑了其他方法名称:resolve()、signal()、genraise()、raiseinto() 和 flush()。这些都不如 throw() 合适。
新的标准异常:GeneratorExit
定义了一个新的标准异常 GeneratorExit,它继承自 Exception。生成器应通过重新引发它(或不捕获它)或引发 StopIteration 来处理它。
新的生成器方法: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
新的生成器方法:__del__()
g.__del__() 是 g.close() 的一个包装器。当生成器对象被垃圾回收时(在 CPython 中,当其引用计数降至零时),将调用此方法。如果 close() 引发异常,则异常的追溯信息将打印到 sys.stderr 并被进一步忽略;它不会传播回触发垃圾回收的位置。这与类实例上 __del__() 方法中异常的处理方式一致。
如果生成器对象参与循环,则可能不会调用 g.__del__()。这是 CPython 当前垃圾收集器的行为。限制的原因是 GC 代码需要在任意点 打破 循环才能收集它,从那时起,任何 Python 代码都不应该被允许看到构成循环的对象,因为它们可能处于无效状态。挂在 循环外的对象不受此限制。
请注意,在实践中不太可能看到生成器对象参与循环。但是,将生成器对象存储在全局变量中会通过生成器帧的 f_globals 指针创建循环。创建循环的另一种方法是将对生成器对象的引用存储在作为参数传递给生成器的数据结构中(例如,如果一个对象有一个作为生成器的方法,并保留了该方法创建的正在运行的迭代器的引用)。鉴于生成器的典型使用模式,这两种情况都不太可能发生。
此外,在本 PEP 的 CPython 实现中,每当生成器因错误或正常退出而终止执行时,应释放生成器使用的帧对象。这将确保不能恢复的生成器不会仍然是不可收集的引用循环的一部分。这允许其他代码可能在 try/finally 或 with 块中使用 close()(根据 PEP 343)以确保给定生成器得到正确终结。
可选扩展
扩展的 continue 语句
本 PEP 的早期草案提议在 for 循环中使用新的 continue EXPR 语法(继承自 PEP 340),它会将 EXPR 的值传递给正在循环的迭代器。此功能暂时已撤回,因为本 PEP 的范围已缩小,仅关注将值传递给生成器迭代器,而不关注其他类型的迭代器。Python-Dev 列表上的一些人也认为,为这个特定功能添加新语法至少是为时过早的。
未解决的问题
python-dev 上的讨论揭示了一些未解决的问题。我在此列出它们,并附上我偏好的解决方案及其动机。当前编写的 PEP 反映了此偏好解决方案。
- 当生成器响应
GeneratorExit异常时,如果它产生了另一个值,close()应该引发什么异常?我最初选择
TypeError,因为它代表生成器函数的严重错误行为,应该通过修改代码来修复。但是 PEP 343 中的with_template装饰器类对类似错误使用了RuntimeError。可以说它们都应该使用相同的异常。我宁愿不为此目的引入一个新的异常类,因为这不是我希望人们捕获的异常:我希望它变成一个追溯信息,由程序员看到并修复代码。所以现在我认为它们都应该引发RuntimeError。这有一些先例:核心 Python 代码在检测到无限递归以及未初始化对象时(以及各种杂项条件)都会引发它。 - Oren Tirosh 提议将
send()方法重命名为feed(),以与 消费者接口 兼容(请参阅 http://effbot.org/zone/consumer.htm 获取规范)。然而,仔细查看消费者接口,似乎
feed()的期望语义与send()不同,因为send()不能在刚启动的生成器上进行有意义的调用。此外,当前定义的消费者接口不包括对StopIteration的处理。因此,创建一个简单的装饰器来包装生成器函数以使其符合消费者接口可能会更有用。例如,它可以 _预热_ 生成器,通过最初的
next()调用,捕获 StopIteration,甚至可以通过重新调用生成器函数来提供reset()。
示例
- 一个简单的 消费者 装饰器,使生成器函数在初始调用时自动前进到其第一个 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
- 一个使用 consumer 装饰器创建 reverse generator 的示例,该生成器接收图像并创建缩略图页面,然后将其发送给另一个消费者。这样的函数可以链接起来,形成高效的 consumers 处理管道,每个消费者都可以拥有复杂的内部状态
@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()
- 一个简单的协程调度器或 trampoline,它允许协程通过 yield 他们希望调用的协程来 调用 其他协程。协程 yield 的任何非生成器值都将返回给 调用 yield 值的协程。同样,如果协程引发异常,异常将传播给其 调用者。实际上,这个例子模拟了 Stackless Python 中使用的简单 tasklet,只要您使用 yield 表达式来调用那些否则会 阻塞 的例程。这只是一个非常简单的示例,并且可以实现更复杂的调度器。(例如,Python 现有的 GTasklet 框架 (http://www.gnome.org/~gjc/gtasklet/gtasklets.html) 和 peak.events 框架 (http://peak.telecommunity.com/) 已经实现了类似的调度功能,但目前必须使用笨拙的变通方法来解决无法将值或异常传递给生成器的问题。)
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)
- 一个简单的 echo 服务器,以及使用 trampoline 运行它的代码(假定存在
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()
参考实现
一个实现本 PEP 中描述的所有功能的原型补丁可在 SourceForge 补丁 #1223381 (https://bugs.python.org/issue1223381) 获取。
此补丁已于 2005 年 8 月 1 日至 2 日提交到 CVS。
致谢
Raymond Hettinger (PEP 288) 和 Samuele Pedroni (PEP 325) 最早正式提出了将值或异常传递给生成器以及 关闭 生成器的想法。Timothy Delaney 提出了本 PEP 的标题,Steven Bethard 帮助编辑了之前的版本。另请参阅 PEP 340 的致谢部分。
参考资料
待定。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0342.rst