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
部分内暂停执行,因此使中止的协程难以自行清理。
此外,生成器无法在其他函数执行时产生控制权,除非这些函数本身以生成器形式表达,并且外部生成器被编写为响应内部生成器产生的值而产生控制权。这使得即使是相对简单的用例(如异步通信)的实现也变得复杂,因为调用任何函数都需要生成器阻塞(即无法产生控制权),或者在每个需要的函数调用周围添加大量样板循环代码。
但是,如果可以在生成器暂停的位置向其传入值或异常,一个简单的协程调度程序或蹦床函数将允许协程调用彼此而不会阻塞——这对于异步应用程序来说是一个巨大的好处。此类应用程序随后可以编写协程来执行非阻塞套接字 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 表达式。此 yield 表达式的值为 None
,除非 send()
使用非 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 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
块(根据PEP 343)中使用close()
来确保给定的生成器得到正确地终结。
可选扩展
扩展的 continue
语句
PEP 的早期草案建议了一种新的continue EXPR
语法,用于在 for 循环中使用(从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 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()
- 一个简单的协程调度器或蹦床,它允许协程通过产出它们希望调用的协程来调用其他协程。协程产出的任何非生成器值都会返回给调用该产出值的协程。类似地,如果协程引发异常,则异常会传播到其调用者。实际上,此示例模拟了 Stackless Python 中使用的简单任务,只要您使用 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)
- 一个简单的回显服务器,以及使用蹦床运行它的代码(假设存在
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
上次修改时间:2023-09-09 17:39:29 GMT