PEP原文 : https://www.python.org/dev/peps/pep-0342/html
PEP标题: Coroutines via Enhanced Generatorspython
PEP做者: Guido van Rossum, Phillip J. Ebygit
建立日期: 2005-05-10程序员
合入版本: 2.5github
译者 :豌豆花下猫(Python猫 公众号做者)算法
简介express
动机编程
规格摘要ruby
规格:将值发送到生成器服务器
新的生成器方法:send(value)
新的语法:yield 表达式
规格:异常和清理
新语法:yield 容许在try-finally
中
新的生成器方法:throw(type,value = None,traceback = None)
新的标准异常:GeneratorExit
新的生成器方法:close()
新的生成器方法:del()
可选的扩展
扩展的 continue 表达式
未决问题
示例
参考实现
致谢
参考文献
版权
这个 PEP 在生成器的 API 和语法方面,提出了一些加强功能,使得它们能够做为简单的协程使用。这基本上是将下述两个 PEP 的想法结合起来,若是它被采纳,那它们就是多余的了:
(译注:PEP-288 和 PEP-325 都没有被采纳经过,它们的核心内容被集成到了 PEP-342里。)
协程是表达许多算法的天然方式,例如模拟/仿真、游戏、异步 I/O、以及其它事件驱动编程或协同的多任务处理。Python 的生成器函数几乎就是协程——但不彻底是——由于它们容许暂停来生成值,但又不容许在程序恢复时传入值或异常。它们也不容许在 try-finally 结构的 try 部分做暂停,所以很难令一个异常退出的(aborted)协程来清理本身。
一样地,当其它函数在执行时,生成器不能提供控制,除非这些函数自己是生成器,而且外部生成器之因此写了去 yield,是要为了响应内部生成器所 yield 的值。这使得即便是相对简单的实现(如异步通讯)也变得复杂,由于调用任意函数,要么须要生成器变堵塞(block,即没法提供控制),要么必须在每一个要调用的函数的周围,添加一大堆引用循环代码(a lot of boilerplate looping code)。
可是,若是有可能在生成器挂起的点上传递进来值或者异常,那么,一个简单的协程调度器或蹦床函数(trampoline function
)就能使协程相互调用且不用阻塞——对异步应用程序有巨大好处。这些应用程序能够编写协程来运行非阻塞的 socket I/O,经过给 I/O 调度器提供控制,直到数据被发送或变为可用。同时,执行 I/O 的代码只需像以下方式操做,就能暂停执行,直到 nonblocking_read() 继续产生一个值:
data = (yield nonblocking_read(my_socket, nbytes))
换句话说, 经过给语言和生成器类型增长一些相对较小的加强,Python 不须要为整个程序编写一系列回调,就能支持异步操做,而且对于本该须要数百上千个协做式的多任务伪线程的(co-operatively multitasking pseudothreads)程序,也能够不须要使用资源密集型线程。所以,这些加强功能将给标准 Python 带来 Stackless Python 的许多优势,又无需对 CPython 核心及其 API 进行任何重大的修改。此外,这些加强在任何已经支持生成器的 Python 实现(例如 Jython)上都是可落实的。
经过给生成器类型增长一些简单的方法,以及两个微小的语法调整,Python 开发者就可以使用生成器函数来实现协程与其它的协做式多任务。这些方法和调整是:
StopIteration
。StopIteration
(若是生成器没有捕获传入的异常,或者它引起了其它异常,则该异常会传递给调用者。)GeneratorExit
。若是生成器在以后引起 StopIteration
(经过正常退出,或者已经被关闭)或 GeneratorExit
(经过不捕获异常),则 close() 返回给其调用者。若是生成器产生一个值,则抛出 RuntimeError
。若是生成器引起任何其它异常,也会传递给调用者。若是生成器已经退出(异常退出或正常退出),则 close() 不执行任何操做。实现了全部这些变动的原型补丁已经可用了,可做为当前 Python CVS HEAD 的 SourceForge 补丁。# 1223381
为生成器提出了一种新的方法,即 send() 。它只接收一个参数,并将它发送给生成器。调用 send(None) 彻底等同于调用生成器的 next() 方法。使用其它参数调用 send() 也有一样的效果,不一样的是,当前生成器表达式产生的值会不同。
由于生成器在生成器函数体的头部执行,因此在刚刚建立生成器时不会有 yield 表达式来接收值,所以,当生成器刚启动时,禁止使用非 None 参数来调用 send() ,若是调用了,就会抛出 TypeError
(多是因为某种逻辑错误)。因此,在与协程通讯前,必须先调用 next() 或 send(None) ,来将程序推动到第一个 yield 表达式。
与 next() 方法同样,send() 方法也返回生成器产生的下一个值,或者抛出 StopIteration
异常(当生成器正常退出,或早已退出时)。若是生成器出现未捕获的异常,则它会传给调用者。
yield 语句(yield-statement)能够被用在赋值表达式的右侧;在这种状况下,它就是 yield 表达式(yield-expression)。除非使用非 None 参数调用 send() ,不然 yield 表达式的值就是 None。见下文。
yield 表达式必须始终用括号括起来,除非它是做为顶级表达式而出如今赋值表达式的右侧。因此,下面例子都是合法的:
x = yield 42
x = yield
x = 12 + (yield 42)
x = 12 + (yield)
foo(yield 42)
foo(yield)
而下面的例子则是非法的(举了一些特例的缘由是,当前的 yield 12,42
是合法的):
x = 12 + yield 42
x = 12 + yield
foo(yield 42, 12)
foo(yield, 12)
请注意,现在没有表达式的 yield-语句 和 yield-表达式是合法的。这意味着:当 next() 调用中的信息流被反转时,应该能够在不传递显式的值的状况下 yield (yield 固然就等同于 yield None)。
当调用 send(value) 时,它恢复的 yield 表达式将返回传入的值。当调用 next() 时,它恢复的 yield 表达式将返回 None。若是 yield-表达式(yield-expression)是一个 yield-语句(yield-statement),其返回值会被忽略,就相似于忽略用做语句的函数的返回值。
实际上,yield 表达式就像一个反函数调用(inverted function);它所 yield 的值其实是当前函数返回(生成)的,而它 return 的值则是经过 send() 传入的参数。
提示:这样的拓展语法,使得它很是地接近于 Ruby。这是故意的。请注意,Python 在阻塞时,经过使用 send(EXPR) 而不是 return EXPR 来传值给生成器,而且在生成器与阻塞之间传递控制权的底层机制彻底不一样。Python 中的阻塞不会被编译成 thunk,相反,yield 暂停生成器的执行进度。有一些不是这样的特例,在 Python 中,你不能保存阻塞以供后续调用,而且你没法测试是否存在着阻塞。(XXX - 关于阻塞的这些东西彷佛不合适,或许 Guido 会编辑下,作澄清。)
让生成器对象成为经过调用生成器函数而生成的迭代器。本节中的 g 指的都是生成器对象。
生成器函数的语法被拓展了,容许在 try-finally 语句中使用 yield 语句。
g.throw(type, value, traceback)
会使生成器在挂起的点处抛出指定的异常(即在 yield 语句中,或在其函数体的头部、且还未调用 next() 时)。若是生成器捕获了异常,并生成了新的值,则它就是 g.throw() 的返回值。若是生成器没有捕获异常,那 throw() 也会抛出一样的异常(它溜走了)。若是生成器抛出其它异常(包括返回时产生的 StopIteration),那该异常会被 throw() 抛出。总之,throw() 的行为相似于 next() 或 send(),除了它是在挂起点处抛出异常。若是生成器已经处于关闭状态,throw() 只会抛出通过它的异常,而不去执行生成器的任何代码。
抛出异常的效果彻底像它所声明的那样:
raise type, value, traceback
会在暂停点执行。type 参数不能是 None,且 type 与 value 的类型必须得兼容。若是 value 不是 type 的实例(instance),则按照 raise 语句建立异常实例的规则,用 value 来生成新的异常实例。若是提供了 traceback 参数,则它必须是有效的 Python 堆栈(traceback)对象,不然会抛出 TypeError 。
注释:选择 throw() 这个名称,有几个缘由。Raise 是一个关键字,所以不能做为方法的名称。与 raise 不一样(它在执行点处即时地抛出异常),throw() 首先恢复生成器,而后才抛出异常。单词 throw 意味着将异常抛在别处,而且跟其它语言相关联。
考虑了几个替代的方法名:resolve()
, signal()
, genraise()
, raiseinto()
和 flush()
。没有一个像 throw() 那般合适。
定义了一个新的标准异常 GeneratorExit,继承自 Exception。生成器应该继续抛出它(或者就不捕获它),或者经过抛出 StopIteration 来处理这个问题。
g.close() 由如下伪代码定义:
def close(self):
try:
self.throw(GeneratorExit)
except (GeneratorExit, StopIteration):
pass
else:
raise RuntimeError("generator ignored GeneratorExit")
# Other exceptions are not caught
g.__ del __()
是 g.close() 的装饰器。当生成器对象被做垃圾回收时,会调用它(在 CPython 中,则是它的引用计数变为零时)。若是 close() 引起异常, 异常的堆栈信息(traceback)会被打印到 sys.stderr 并被忽略掉;它不会退回到触发垃圾回收的地方。这与类实例在处理 __del__()
的异常时的方法同样。
若是生成器对象被循环引用,则可能不会调用 g.__del__()
。这是当前 CPython 的垃圾收集器的表现。作此限制的缘由是,GC 代码须要在一个任意点打破循环,以便回收它,在此以后,不容许 Python 代码“看到”造成循环的对象,由于它们可能处于无效的状态。被用于解开(hanging off)循环的对象不受此限制。
尽管实际上不太可能看到生成器被循环引用。可是,若将生成器对象存储在全局变量中,则会经过生成器框架的 f_globals 指针建立一个循环。另外,若在数据结构中存储对生成器对象的引用,且该数据结构被做为参数传递给生成器,这也会创造一个循环引用(例如,若是一个对象具备一个做为生成器的方法,并持有由该方法建立的运行中的迭代器的引用)。鉴于生成器的典型用法,这些状况都不太可能。
此外,CPython 在实现当前 PEP 时,每当因为错误或正常退出而终止执行时,会释放被生成器使用的框架对象(frame object)。这保证了那些没法被恢复的生成器不会成为没法回收的循环引用的部分。这就容许了其它代码在 try-finally 或 with 语句中使用 close() (参考 PEP-343),确保了给定的生成器会正确地完结。
本 PEP 的早期草案提出了一种新的 continue EXPR 语法,用于 for 循环(继承自 PEP-340),将 EXPR 的值传给被遍历的迭代器。此功能暂时被撤销了,由于本 PEP 的范围已经缩小,只关注将值传给生成器迭代器(generator-iterator),而非其它类型的迭代器。Python-Dev 邮件列表中的一些人也以为为这个特定功能添加新语法是为时过早(would be premature at best)。
Python-Dev 邮件的讨论提出了一些未决的问题。我罗列于此,附上我推荐的解决方案与它的动机。目前编写的 PEP 也反映了这种喜爱的解决方案。
当生成器产生另外一个值做为对“GeneratorExit”异常的响应时,close()
应该引起什么异常?
我最初选择了 TypeError ,由于它表示生成器函数发生了严重的错误行为,应该经过修改代码来修复。可是 PEP-343 中的 with_template
装饰器类使用了 RuntimeError 来进行相似处理。能够说它们都应该使用相同的异常。我宁愿不为此目的引入新的异常类,由于它不是我但愿人们捕获的异常:我但愿它变成一个 traceback 给程序员看到,而后进行修复。因此我以为它们都应该抛出 RuntimeError 。有一些先例:在检测到无限递归的状况下,或者检测到未初始化的对象(因为各类各样的缘由),核心 Python 代码会抛出该异常。
Oren Tirosh 建议将 send() 方法重命名为 feed() ,以便能跟 consumer 接口兼容(规范参见:http://effbot.org/zone/consumer.htm)。
然而,仔细观察 consumer 接口,彷佛 feed() 所需的语义与 send() 不一样,由于后者不能在刚启动的生成器上做有意义的调用。此外,当前定义的 consumer 接口不包含对 StopIteration 的处理。
所以,建立一个贴合 consumer 接口的简单的装饰器,来装饰生成器函数,彷佛会更有用。举个例子,它能够用初始的 next() 调用给生成器预热(warm up),追踪 StopIteration,甚至能够经过从新调用生成器来提供 reset() 用途。
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()
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)。
该补丁已提交到 CVS,2005年8月 01-02。
Raymond Hettinger (PEP 288) 与 Samuele Pedroni (PEP 325) 第一个正式地提出将值或异常传递给生成器的想法,以及关闭生成器的能力。Timothy Delaney 建议了本 PEP 的标题,还有 Steven Bethard 帮忙编辑了早期的版本。另见 PEP-340 的致谢部分。
TBD.
本文档已经放置在公共领域。
源文档:https://github.com/python/peps/blob/master/pep-0342.txt
----------------(译文完)--------------------
相关连接:
PEP背景知识 :学习Python,怎能不懂点PEP呢?
PEP翻译计划 :https://github.com/chinesehuazhou/peps-cn
花下猫语: 唠叨几句吧,年前这几周事情太多了,挤着时间好歹是又翻译出一篇 PEP。与生成器密切相关的 PEP 已经完成 3/4,年后再译最后一篇(PEP-380)。当初翻译第一篇,彻底是一时兴起,直觉这是一件有意义的事,如今呢,这个念头开始有点膨胀——我居然在 Github 上建了个翻译项目。我深知,本身水平实在有限,所以不求获得多少认同吧。但行好事,莫问前程。不过,如有人帮着吆喝一声,也是极好的。
-----------------
本文原创并首发于微信公众号【Python猫】,后台回复“爱学习”,免费得到20+本精选电子书。