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
動機編程
規格摘要服務器
規格:將值發送到生成器數據結構
規格:異常和清理
try-finally
中__del__()
可選的擴展
未決問題
示例
參考實現
致謝
參考文獻
版權
這個 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
__del__()
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 上建了個翻譯項目。我深知,本身水平實在有限,所以不求獲得多少認同吧。但行好事,莫問前程。不過,如有人幫着吆喝一聲,也是極好的。
-----------------
本文原創並首發於wx公衆號【Python貓】,後臺回覆「愛學習」,免費得到20+本精選電子書。