摘要:第四種選擇是在不同的線程中運行生產(chǎn)者和消費者。包含語句的函數(shù)被稱為生成器函數(shù)。然后引發(fā)一個異常,表明迭代器已經(jīng)耗盡。換句話說,未捕獲的異常終結(jié)了生成器的使用壽命。
我正打算寫寫 Python 的生成器,然而查資料時發(fā)現(xiàn),引入生成器的 PEP 沒人翻譯過,因此就花了點時間翻譯出來。如果在閱讀時,你有讀不懂的地方,不用懷疑,極有可能是我譯得不到位。若出現(xiàn)這種情況,我建議你直接閱讀原文,最好也能將錯誤處告知于我,以便做出修改。
原文:https://www.python.org/dev/pe...
創(chuàng)建日期:2001-05-18
合入Python版本:2.2
譯者 :豌豆花下貓(Python貓 公眾號作者)
PEP背景知識 :學(xué)習(xí)Python,怎能不懂點PEP呢?
摘要這個 PEP 想在 Python 中引入生成器的概念,以及一個新的表達式,即 yield 表達式。
動機當(dāng)一個生產(chǎn)者函數(shù)在處理某些艱難的任務(wù)時,它可能需要維持住生產(chǎn)完某個值時的狀態(tài),大多數(shù)編程語言都提供不了既舒服又高效的方案,除了往參數(shù)列表中添加回調(diào)函數(shù),然后每生產(chǎn)一個值時就去調(diào)用一下。
例如,標(biāo)準(zhǔn)庫中的tokenize.py采用這種方法:調(diào)用者必須傳一個 tokeneater 函數(shù)給 tokenize() ,當(dāng) tokenize() 找到下一個 token 時再調(diào)用。這使得 tokenize 能以自然的方式編碼,但程序調(diào)用 tokenize 會變得極其復(fù)雜,因為它需要記住每次回調(diào)前最后出現(xiàn)的是哪個 token(s)。tabnanny.py中的 tokeneater 函數(shù)是處理得比較好的例子,它在全局變量中維護了一個狀態(tài)機,用于記錄已出現(xiàn)的 token 和預(yù)期會出現(xiàn)的 token 。這很難正確地工作,而且也挺難讓人理解。不幸的是,它已經(jīng)是最標(biāo)準(zhǔn)的解決方法了。
有一個替代方案是一次性生成 Python 程序的全部解析,并存入超大列表中。這樣 tokenize 客戶端可以用自然的方式,即使用局部變量和局部控制流(例如循環(huán)和嵌套的 if 語句),來跟蹤其狀態(tài)。然而這并不實用:程序會變得臃腫,因此不能在實現(xiàn)整個解析所需的內(nèi)存上放置先驗限制;而有些 tokenize 客戶端僅僅想要查看某個特定的東西是否曾出現(xiàn)(例如,future 聲明,或者像 IDLE 做的那樣,只是首個縮進的聲明),因此解析整個程序就是嚴重地浪費時間。
另一個替代方案是把 tokenize 變?yōu)橐粋€迭代器【注釋1】,每次調(diào)用它的 next() 方法時再傳遞下一個 token。這對調(diào)用者來說很便利,就像前一方案把結(jié)果存入大列表一樣,同時沒有內(nèi)存與“想要早點退出怎么辦”的缺點。然而,這個方案也把 tokenize 的負擔(dān)轉(zhuǎn)化成記住 next() 的調(diào)用狀態(tài),讀者只要瞄一眼 tokenize.tokenize_loop() ,就會意識到這是一件多么可怕的苦差事?;蛘呦胂笠幌?,用遞歸算法來生成普通樹結(jié)構(gòu)的節(jié)點:若把它投射成一個迭代器框架實現(xiàn),就需要手動地移除遞歸狀態(tài)并維護遍歷的狀態(tài)。
第四種選擇是在不同的線程中運行生產(chǎn)者和消費者。這允許兩者以自然的方式維護其狀態(tài),所以都會很舒服。實際上,Python 源代碼發(fā)行版中的 Demo/threads/Generator.py 就提供了一個可用的同步通信(synchronized-communication)類,來完成一般的任務(wù)。但是,這在沒有線程的平臺上無法運用,而且就算可用也會很慢(與不用線程可取得的成就相比)。
最后一個選擇是使用 Python 的變種 Stackless 【注釋2-3】來實現(xiàn),它支持輕量級的協(xié)程。它與前述的線程方案有相同的編程優(yōu)勢,效率還更高。然而,Stackless 在 Python 核心層存在爭議,Jython 也可能不會實現(xiàn)相同的語義。這個 PEP 不是討論這些問題的地方,但完全可以說生成器是 Stackless 相關(guān)功能的子集在當(dāng)前 CPython 中的一種簡單實現(xiàn),而且可以說,其它 Python 實現(xiàn)起來也相對簡單。
以上分析完了已有的方案。其它一些高級語言也提供了不錯的解決方案,特別是 Sather 的迭代器,它受到 CLU 的迭代器啟發(fā)【注釋4】;Icon 的生成器,一種新穎的語言,其中每個表達式都是生成器【注釋5】。它們雖有差異,但基本的思路是一致的:提供一種函數(shù),它可以返回中間結(jié)果(“下一個值”)給它的調(diào)用者,同時還保存了函數(shù)的局部狀態(tài),以便在停止的位置恢復(fù)(譯注:resum,下文也譯作激活)調(diào)用。一個非常簡單的例子:
def fib(): a, b = 0, 1 while 1: yield b a, b = b, a+b
當(dāng) fib() 首次被調(diào)用時,它將 a 設(shè)為 0,將 b 設(shè)為 1,然后生成 b 給其調(diào)用者。調(diào)用者得到 1。當(dāng) fib 恢復(fù)時,從它的角度來看,yield 語句實際上跟 print 語句相同:fib 繼續(xù)執(zhí)行,且所有局部狀態(tài)完好無損。然后,a 和 b 的值變?yōu)?1,并且 fib 再次循環(huán)到 yield,生成 1 給它的調(diào)用者。以此類推。 從 fib 的角度來看,它只是提供一系列結(jié)果,就像用了回調(diào)一樣。但是從調(diào)用者的角度來看,fib 的調(diào)用就是一個可隨時恢復(fù)的可迭代對象。跟線程一樣,這允許兩邊以最自然的方式進行編碼;但與線程方法不同,這可以在所有平臺上高效完成。事實上,恢復(fù)生成器應(yīng)該不比函數(shù)調(diào)用昂貴。
同樣的方法適用于許多生產(chǎn)者/消費者函數(shù)。例如,tokenize.py 可以生成下一個 token 而不是用它作為參數(shù)調(diào)用回調(diào)函數(shù),而且 tokenize 客戶端可以以自然的方式迭代 tokens:Python 生成器是一種迭代器,但是特別強大。
設(shè)計規(guī)格:yield引入了一種新的表達式:
yield_stmt:“yield”expression_list
yield 是一個新的關(guān)鍵字,因此需要一個 future 聲明【注釋8】來進行引入:在早期版本中,若想使用生成器的模塊,必須在接近頭部處包含以下行(詳見 PEP 236):
from __future__ import generators
沒有引入 future 模塊就使用 yield 關(guān)鍵字,將會告警。 在后續(xù)的版本中,yield 將是一個語言關(guān)鍵字,不再需要 future 語句。
yield 語句只能在函數(shù)內(nèi)部使用。包含 yield 語句的函數(shù)被稱為生成器函數(shù)。從各方面來看,生成器函數(shù)都只是個普通函數(shù),但在它的代碼對象的 co_flags 中設(shè)置了新的“CO_GENERATOR”標(biāo)志。
當(dāng)調(diào)用生成器函數(shù)時,實際參數(shù)還是綁定到函數(shù)的局部變量空間,但不會執(zhí)行代碼。得到的是一個 generator-iterator 對象;這符合迭代器協(xié)議【注釋6】,因此可用于 for 循環(huán)。注意,在上下文無歧義的情況下,非限定名稱 “generator” 既可以指生成器函數(shù),又可以指生成器-迭代器(generator-iterator)。
每次調(diào)用 generator-iterator 的 next() 方法時,才會執(zhí)行 generator-function 體中的代碼,直至遇到 yield 或 return 語句(見下文),或者直接迭代到盡頭。
如果執(zhí)行到 yield 語句,則函數(shù)的狀態(tài)會被凍結(jié),并將 expression_list 的值返回給 next() 的調(diào)用者。“凍結(jié)”是指掛起所有本地狀態(tài),包括局部變量、指令指針和內(nèi)部堆棧:保存足夠的信息,以便在下次調(diào)用 next() 時,函數(shù)可以繼續(xù)執(zhí)行,仿佛 yield 語句只是一次普通的外部調(diào)用。
限制:yield 語句不能用于 try-finally 結(jié)構(gòu)的 try 子句中。困難的是不能保證生成器會被再次激活(resum),因此無法保證 finally 語句塊會被執(zhí)行;這就太違背 finally 的用處了。
限制:生成器在活躍狀態(tài)時無法被再次激活:
>>> def g(): ... i = me.next() ... yield i >>> me = g() >>> me.next() Traceback (most recent call last): ... File "設(shè)計規(guī)格:return", line 2, in g ValueError: generator already executing
生成器函數(shù)還可以包含以下形式的return語句:
return
注意,生成器主體中的 return 語句不允許使用 expression_list (然而當(dāng)然,它們可以嵌套地使用在生成器里的非生成器函數(shù)中)。
當(dāng)執(zhí)行到 return 語句時,程序會正常 return,繼續(xù)執(zhí)行恰當(dāng)?shù)?finally 子句(如果存在)。然后引發(fā)一個 StopIteration 異常,表明迭代器已經(jīng)耗盡。如果程序沒有顯式 return 而執(zhí)行到生成器的末尾,也會引發(fā) StopIteration 異常。
請注意,對于生成器函數(shù)和非生成器函數(shù),return 意味著“我已經(jīng)完成,并且沒有任何有趣的東西可以返回”。
注意,return 并不一定會引發(fā) StopIteration :關(guān)鍵在于如何處理封閉的 try-except 結(jié)構(gòu)。 例如:
>>> def f1(): ... try: ... return ... except: ... yield 1 >>> print list(f1()) []
因為,就像在任何函數(shù)中一樣,return 只是退出,但是:
>>> def f2(): ... try: ... raise StopIteration ... except: ... yield 42 >>> print list(f2()) [42]
因為 StopIteration 被一個簡單的 except 捕獲,就像任意異常一樣。
設(shè)計規(guī)格:生成器和異常傳播如果一個未捕獲的異常——包括但不限于 StopIteration——由生成器函數(shù)引發(fā)或傳遞,則異常會以通常的方式傳遞給調(diào)用者,若試圖重新激活生成器函數(shù)的話,則會引發(fā) StopIteration 。 換句話說,未捕獲的異常終結(jié)了生成器的使用壽命。
示例(不合語言習(xí)慣,僅作舉例):
>>> def f(): ... return 1/0 >>> def g(): ... yield f() # the zero division exception propagates ... yield 42 # and we"ll never get here >>> k = g() >>> k.next() Traceback (most recent call last): File "設(shè)計規(guī)格:Try/Exception/Finally", line 1, in ? File " ", line 2, in g File " ", line 2, in f ZeroDivisionError: integer division or modulo by zero >>> k.next() # and the generator cannot be resumed Traceback (most recent call last): File " ", line 1, in ? StopIteration >>>
前面提過,yield 語句不能用于 try-finally 結(jié)構(gòu)的 try 子句中。這帶來的結(jié)果是生成器要非常謹慎地分配關(guān)鍵的資源。但是在其它地方,yield 語句并無限制,例如 finally 子句、except 子句、或者 try-except 結(jié)構(gòu)的 try 子句:
>>> def f(): ... try: ... yield 1 ... try: ... yield 2 ... 1/0 ... yield 3 # never get here ... except ZeroDivisionError: ... yield 4 ... yield 5 ... raise ... except: ... yield 6 ... yield 7 # the "raise" above stops this ... except: ... yield 8 ... yield 9 ... try: ... x = 12 ... finally: ... yield 10 ... yield 11 >>> print list(f()) [1, 2, 4, 5, 8, 9, 10, 11] >>>示例
# 二叉樹類 class Tree: def __init__(self, label, left=None, right=None): self.label = label self.left = left self.right = right def __repr__(self, level=0, indent=" "): s = level*indent + `self.label` if self.left: s = s + " " + self.left.__repr__(level+1, indent) if self.right: s = s + " " + self.right.__repr__(level+1, indent) return s def __iter__(self): return inorder(self) # 從列表中創(chuàng)建 Tree def tree(list): n = len(list) if n == 0: return [] i = n / 2 return Tree(list[i], tree(list[:i]), tree(list[i+1:])) # 遞歸生成器,按順序生成樹標(biāo)簽 def inorder(t): if t: for x in inorder(t.left): yield x yield t.label for x in inorder(t.right): yield x # 展示:創(chuàng)建一棵樹 t = tree("ABCDEFGHIJKLMNOPQRSTUVWXYZ") # 按順序打印樹的節(jié)點 for x in t: print x, print # 非遞歸生成器 def inorder(node): stack = [] while node: while node.left: stack.append(node) node = node.left yield node.label while not node.right: try: node = stack.pop() except IndexError: return yield node.label node = node.right # 練習(xí)非遞歸生成器 for x in t: print x, print Both output blocks display: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z問答 為什么重用 def 而不用新的關(guān)鍵字?
請參閱下面的 BDFL 聲明部分。
為什么用新的關(guān)鍵字yield而非內(nèi)置函數(shù)?Python 中通過關(guān)鍵字能更好地表達控制流,即 yield 是一個控制結(jié)構(gòu)。而且為了 Jython 的高效實現(xiàn),編譯器需要在編譯時就確定潛在的掛起點,新的關(guān)鍵字會使這一點變得簡單。CPython 的實現(xiàn)也大量利用它來檢測哪些函數(shù)是生成器函數(shù)(盡管一個新的關(guān)鍵字替代 def 就能解決 CPython 的問題,但人們問“為什么要新的關(guān)鍵字”問題時,并不想要新的關(guān)鍵字)。
為什么不是其它不帶新關(guān)鍵字的特殊語法?例如,為何不用下面用法而用 yield 3:
return 3 and continue return and continue 3 return generating 3 continue return 3 return >> , 3 from generator return 3 return >> 3 return << 3 >> 3 << 3 * 3
我沒有錯過一個“眼色”吧?在數(shù)百條消息中,我算了每種替代方案有三條建議,然后總結(jié)出上面這些。不需要用新的關(guān)鍵字會很好,但使用 yield 會更好——我個人認為,在一堆無意義的關(guān)鍵字或運算符序列中,yield 更具表現(xiàn)力。盡管如此,如果這引起足夠的興趣,支持者應(yīng)該發(fā)起一個提案,交給 Guido 裁斷。
為什么允許用return,而不強制用StopIteration?“StopIteration”的機制是底層細節(jié),就像 Python 2.1 中的“IndexError”的機制一樣:實現(xiàn)時需要做一些預(yù)先定義好的東西,而 Python 為高級用戶開放了這些機制。盡管不強制要求每個人都在這個層級工作。 “return”在任何一種函數(shù)中都意味著“我已經(jīng)完成”,這很容易解讀和使用。注意,return 并不總是等同于 try-except 結(jié)構(gòu)中的 raise StopIteration(參見“設(shè)計規(guī)格:Return”部分)。
那為什么不允許return一個表達式?也許有一天會允許。 在 Icon 中,return expr 意味著“我已經(jīng)完成”和“但我還有最后一個有用的值可以返回,這就是它”。 在初始階段,不強制使用return expr的情況下,使用 yield 僅僅傳遞值,這很簡單明了。
BDFL聲明 Issue引入另一個新的關(guān)鍵字(比如,gen 或 generator )來代替 def ,或以其它方式改變語法,以區(qū)分生成器函數(shù)和非生成器函數(shù)。
Con實際上(你如何看待它們),生成器是函數(shù),但它們具有可恢復(fù)性。使它們建立起來的機制是一個相對較小的技術(shù)問題,引入新的關(guān)鍵字無助于強調(diào)生成器是如何啟動的機制(生成器生命中至關(guān)重要卻很小的部分)。
Pro實際上(你如何看待它們),生成器函數(shù)實際上是工廠函數(shù),它們就像施了魔法一樣地生產(chǎn)生成器-迭代器。 在這方面,它們與非生成器函數(shù)完全不同,更像是構(gòu)造函數(shù)而不是函數(shù),因此重用 def 無疑是令人困惑的。藏在內(nèi)部的 yield 語句不足以警示它們的語義是如此不同。
BDFLdef 留了下來。任何一方都沒有任何爭論是完全令人信服的,所以我咨詢了我的語言設(shè)計師的直覺。它告訴我 PEP 中提出的語法是完全正確的——不是太熱,也不是太冷。但是,就像希臘神話中的 Delphi(譯注:特爾斐,希臘古都) 的甲骨文一樣,它并沒有告訴我原因,所以我沒有對反對此 PEP 語法的論點進行反駁。 我能想出的最好的(除了已經(jīng)同意做出的反駁)是“FUD”(譯注:縮寫自 fear、uncertainty 和 doubt)。 如果這從第一天開始就是語言的一部分,我非常懷疑這早已讓安德魯·庫奇林(Andrew Kuchling)的“Python Warts”頁面成為可能。(譯注:wart 是疣,一種難看的皮膚病。這是一個 wiki 頁面,列舉了對 Python 吹毛求疵的建議)。
參考實現(xiàn)當(dāng)前的實現(xiàn)(譯注:2001年),處于初步狀態(tài)(沒有文檔,但經(jīng)過充分測試,可靠),是Python 的 CVS 開發(fā)樹【注釋9】的一部分。 使用它需要您從源代碼中構(gòu)建 Python。
這是衍生自 Neil Schemenauer【注釋7】的早期補丁。
腳注和參考文獻[1] PEP-234, Iterators, Yee, Van Rossum
http://www.python.org/dev/pep...
[2] http://www.stackless.com/
[3] PEP-219, Stackless Python, McMillan
http://www.python.org/dev/pep...
[4] "Iteration Abstraction in Sather" Murer, Omohundro, Stoutamire and Szyperski
http://www.icsi.berkeley.edu/...
[5] http://www.cs.arizona.edu/icon/
[6] The concept of iterators is described in PEP 234. See [1] above.
[7] http://python.ca/nas/python/g...
[8] PEP 236, Back to the __future__, Peters
http://www.python.org/dev/pep...
[9] To experiment with this implementation, check out Python from CVS according to the instructions at http://sf.net/cvs/?group_id=5470 ,Note that the std test Lib/test/test_generators.py contains many examples, including all those in this PEP.
版權(quán)信息本文檔已經(jīng)放置在公共領(lǐng)域。源文檔:https://github.com/python/peps/blob/master/pep-0255.txt
(譯文完)
PS:官方 PEP 有將近500個,然而保守估計,被翻譯成中文的不足20個(去重的情況下)。我好奇,感興趣將一些重要的 PEP 翻譯出來的人有多少呢?現(xiàn)拋此問題出來探探路,歡迎留言交流。
-----------------
本文翻譯并首發(fā)于微信公眾號【Python貓】,后臺回復(fù)“愛學(xué)習(xí)”,免費獲得20+本精選電子書。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/43035.html
摘要:提議以下的新的生成器語法將被允許在生成器的內(nèi)部使用其中表達式作用于可迭代對象,從迭代器中提取元素。子迭代器而非生成器的語義被選擇成為生成器案例的合理泛化。建議如果關(guān)閉一個子迭代器時,引發(fā)了帶返回值的異常,則將該值從調(diào)用中返回給委托生成器。 導(dǎo)語: PEP(Python增強提案)幾乎是 Python 社區(qū)中最重要的文檔,它們提供了公告信息、指導(dǎo)流程、新功能的設(shè)計及使用說明等內(nèi)容。對于學(xué)習(xí)...
閱讀 4301·2021-11-17 09:33
閱讀 3420·2021-10-08 10:05
閱讀 3350·2021-09-22 15:36
閱讀 1307·2021-09-06 15:02
閱讀 2912·2019-08-29 12:45
閱讀 1723·2019-08-26 13:40
閱讀 3583·2019-08-26 13:37
閱讀 568·2019-08-26 13:37