PEP 255 – 簡單的生成器
- 作者:
- Neil Schemenauer <nas at arctrix.com>, Tim Peters <tim.peters at gmail.com>, Magnus Lie Hetland <magnus at hetland.org>
- 狀態:
- 最終版
- 型別:
- 標準跟蹤
- 要求:
- 234
- 建立日期:
- 2001年5月18日
- Python 版本:
- 2.2
- 釋出歷史:
- 2001年6月14日,2001年6月23日
摘要
本PEP將生成器的概念引入Python,以及與它們一起使用的新語句,即 yield 語句。
動機
當生產者函式為了在生成值之間保持狀態而需要進行足夠複雜的工作時,大多數程式語言除了在生產者的引數列表中添加回調函式(每個生成的值都會呼叫該函式)之外,沒有提供令人愉快且高效的解決方案。
例如,標準庫中的 tokenize.py 採用了這種方法:呼叫者必須將一個 tokeneater 函式傳遞給 tokenize(),每當 tokenize() 找到下一個標記時,都會呼叫該函式。這使得 tokenize 可以以自然的方式編碼,但呼叫 tokenize 的程式通常會因需要在回撥之間記住上次看到了哪些標記而變得複雜。tabnanny.py 中的 tokeneater 函式就是一個很好的例子,它在全域性變數中維護一個狀態機,以便在回撥之間記住它已經看到了什麼以及它希望接下來看到什麼。這很難正確執行,而且人們仍然難以理解。不幸的是,這種方法通常如此。
另一種選擇是讓 tokenize 一次性生成整個Python程式的解析結果,儲存在一個大型列表中。然後 tokenize 客戶端就可以以自然的方式編寫,使用區域性變數和區域性控制流(如迴圈和巢狀的if語句)來跟蹤它們的狀態。但這不切實際:程式可能非常大,因此無法預先確定具體化整個解析所需的記憶體;而且一些 tokenize 客戶端只希望檢視程式早期是否出現特定內容(例如,一個未來語句,或者像IDLE中那樣,只是第一個縮排語句),那麼首先解析整個程式會嚴重浪費時間。
另一個替代方案是讓 tokenize 成為一個 迭代器,每當呼叫其 .next() 方法時,就提供下一個標記。這對呼叫者來說與一個大型結果列表一樣令人愉快,但沒有記憶體和“如果我想提前退出怎麼辦?”的缺點。然而,這會將負擔轉移到 tokenize 上,使其在 .next() 呼叫之間記住 其 狀態,讀者只需瞥一眼 tokenize.tokenize_loop() 就會意識到那將是多麼可怕的苦差事。或者想象一個用於生成通用樹結構節點的遞迴演算法:要將其轉換為迭代器框架,需要手動移除遞歸併手動維護遍歷的狀態。
第四種選擇是在單獨的執行緒中執行生產者和消費者。這使得兩者都能以自然的方式維護其狀態,因此對兩者都令人愉快。事實上,Python原始碼分發中的 Demo/threads/Generator.py 提供了一個可用的同步通訊類,可以以通用方式實現這一點。然而,這在沒有執行緒的平臺上不起作用,並且在有執行緒的平臺上(與無需執行緒可實現的效果相比)速度非常慢。
最後一個選擇是改用 Stackless [1] (PEP 219) 變體實現Python,它支援輕量級協程。這與執行緒選項具有大致相同的程式設計優勢,但效率更高。然而,Stackless是對Python核心的一種有爭議的重新思考,Jython可能無法實現相同的語義。本PEP不適合討論這一點,所以這裡只需說,生成器以一種易於融入當前CPython實現的方式提供了Stackless功能的有用子集,並且被認為對其他Python實現來說相對簡單。
目前的替代方案已窮盡。其他一些高階語言提供了令人愉快的解決方案,特別是Sather [2] 中的迭代器,其靈感來自CLU中的迭代器;以及Icon [3] 中的生成器,Icon是一種新穎的語言,其中每個表示式 都是一個生成器。這些之間存在差異,但基本思想是相同的:提供一種可以向其呼叫者返回中間結果(“下一個值”)的函式,但同時保持該函式的區域性狀態,以便該函式可以從上次中斷的地方繼續執行。一個非常簡單的例子
def fib():
a, b = 0, 1
while 1:
yield b
a, b = b, a+b
當 fib() 首次被呼叫時,它將 a 設定為 0,將 b 設定為 1,然後將 b yield 回其呼叫者。呼叫者看到 1。當 fib 恢復時,從它的角度看,yield 語句實際上與,比如說,一個 print 語句相同:fib 在 yield 之後繼續執行,所有區域性狀態都完好無損。a 和 b 接著變為 1 和 1,然後 fib 迴圈回到 yield,向其呼叫者 yield 1。依此類推。從 fib 的角度看,它只是像透過回撥一樣提供一系列結果。但從其呼叫者的角度看,fib 呼叫是一個可迭代物件,可以隨意恢復。與執行緒方法一樣,這允許雙方以最自然的方式編寫程式碼;但與執行緒方法不同,這可以高效地在所有平臺上完成。事實上,恢復生成器的開銷不應超過函式呼叫。
相同的方法適用於許多生產者/消費者函式。例如,tokenize.py 可以 yield 下一個標記,而不是將其作為引數呼叫回撥函式,並且 tokenize 客戶端可以以自然的方式迭代標記:Python 生成器是一種 Python 迭代器,但它是一種特別強大的迭代器。
規範:yield
引入新語句
yield_stmt: "yield" expression_list
yield 是一個新關鍵字,因此需要一個 future 語句 (PEP 236) 來逐步引入它:在初始釋出中,希望使用生成器的模組必須在檔案頂部附近包含以下行
from __future__ import generators
(詳情請參閱 PEP 236)。未使用 future 語句而使用識別符號 yield 的模組將觸發警告。在下一個版本中,yield 將成為語言關鍵字,並且不再需要 future 語句。
yield 語句只能在函式內部使用。包含 yield 語句的函式稱為生成器函式。生成器函式在所有方面都是一個普通的函式物件,但其程式碼物件的 co_flags 成員中設定了新的 CO_GENERATOR 標誌。
當呼叫生成器函式時,實際引數以通常的方式繫結到函式區域性形式引數名稱,但函式體中的程式碼不會執行。相反,返回一個生成器迭代器物件;這符合 迭代器協議,因此特別可以以自然的方式在 for 迴圈中使用。請注意,當從上下文中意圖明確時,不加限定的名稱“生成器”可以指生成器函式或生成器迭代器。
每次呼叫生成器迭代器的 .next() 方法時,生成器函式體中的程式碼都會執行,直到遇到 yield 或 return 語句(見下文),或者直到到達函式體末尾。
如果遇到 yield 語句,函式的狀態將被凍結,並且 expression_list 的值將返回給 .next() 的呼叫者。所謂“凍結”,我們是指所有區域性狀態都被保留,包括區域性變數的當前繫結、指令指標和內部評估棧:儲存了足夠的資訊,以便下次呼叫 .next() 時,函式可以像 yield 語句只是另一個外部呼叫一樣繼續執行。
限制:yield 語句不允許出現在 try/finally 構造的 try 子句中。困難在於無法保證生成器會被恢復,因此無法保證 finally 塊會被執行;這太違反 finally 的目的了。
限制:生成器在活動執行時不能恢復
>>> def g():
... i = me.next()
... yield i
>>> me = g()
>>> me.next()
Traceback (most recent call last):
...
File "<string>", line 2, in g
ValueError: generator already executing
規範:return
生成器函式還可以包含以下形式的 return 語句
return
請注意,在生成器函式體內的 return 語句中不允許使用 expression_list(儘管它們當然可以出現在巢狀在生成器中的非生成器函式體內)。
當遇到 return 語句時,控制流程像任何函式返回一樣繼續,執行相應的 finally 子句(如果存在)。然後會引發 StopIteration 異常,表示迭代器已耗盡。如果控制流程在沒有顯式 return 的情況下流出生成器末尾,也會引發 StopIteration 異常。
請注意,對於生成器函式和非生成器函式,return 都表示“我完成了,並且沒有什麼有趣的東西可以返回”。
請注意,return 並不總是等同於引發 StopIteration:區別在於如何處理外部 try/except 構造。例如,
>>> def f1():
... try:
... return
... except:
... yield 1
>>> print list(f1())
[]
因為,和任何函式一樣,return 只是退出,但是
>>> def f2():
... try:
... raise StopIteration
... except:
... yield 42
>>> print list(f2())
[42]
因為 StopIteration 會被裸 except 捕獲,就像任何異常一樣。
規範:生成器和異常傳播
如果生成器函式引發未處理的異常(包括但不限於 StopIteration),或者異常透過生成器函式傳播,則異常以通常的方式傳遞給呼叫者,並且隨後嘗試恢復生成器函式會引發 StopIteration。換句話說,未處理的異常會終止生成器的有用生命。
示例(非慣用但旨在說明要點)
>>> 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 "<stdin>", line 1, in ?
File "<stdin>", line 2, in g
File "<stdin>", 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 "<stdin>", line 1, in ?
StopIteration
>>>
規範:Try/Except/Finally
如前所述,yield 不允許出現在 try/finally 構造的 try 子句中。這意味著生成器應該非常小心地分配關鍵資源。在其他情況下,yield 出現在 finally 子句、except 子句或 try/except 構造的 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]
>>>
示例
# A binary tree class.
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 + "\n" + self.left.__repr__(level+1, indent)
if self.right:
s = s + "\n" + self.right.__repr__(level+1, indent)
return s
def __iter__(self):
return inorder(self)
# Create a Tree from a list.
def tree(list):
n = len(list)
if n == 0:
return []
i = n / 2
return Tree(list[i], tree(list[:i]), tree(list[i+1:]))
# A recursive generator that generates Tree labels in in-order.
def inorder(t):
if t:
for x in inorder(t.left):
yield x
yield t.label
for x in inorder(t.right):
yield x
# Show it off: create a tree.
t = tree("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
# Print the nodes of the tree in in-order.
for x in t:
print x,
print
# A non-recursive generator.
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
# Exercise the non-recursive generator.
for x in t:
print x,
print
兩個輸出塊都顯示
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?
請參閱下面的 BDFL 宣告部分。
為什麼 yield 需要一個新關鍵字?為什麼不將其作為內建函式?
在 Python 中,控制流最好透過關鍵字來表達,而 yield 是一種控制結構。人們也認為,Jython 中的高效實現要求編譯器能夠在編譯時確定潛在的掛起點,而一個新的關鍵字可以輕鬆實現這一點。CPython 參考實現也大量利用了它,以檢測哪些函式 是 生成器函式(儘管用一個新關鍵字代替 def 可以為 CPython 解決這個問題——但提出“為什麼是一個新關鍵字?”問題的人不想要任何新關鍵字)。
那麼為什麼不使用其他特殊語法而不引入新關鍵字呢?
例如,這些中的一個而不是 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
我漏掉了一個嗎 <眨眼>?在數百條訊息中,我統計到三條建議這種替代方案,並從中提取了上述內容。不使用新關鍵字會很好,但讓 yield 非常清晰會更好——我不想透過理解以前毫無意義的關鍵字或運算子序列來 推斷 正在發生 yield。不過,如果這引起了足夠的興趣,支持者應該達成一個單一的共識建議,Guido 將對此發表意見。
為什麼允許使用 return?為什麼不強制使用 raise StopIteration 來終止?
StopIteration 的機制是低階細節,很像 Python 2.1 中 IndexError 的機制:底層實現需要做 一些 定義明確的事情,而 Python 向高階使用者公開了這些機制。但這並不是強制每個人都在那個級別工作的論據。return 在任何型別的函式中都表示“我完成了”,這很容易解釋和使用。請注意,在 try/except 構造中,return 也不總是等同於 raise StopIteration(參見“規範:Return”部分)。
那麼為什麼也不允許 return 後面跟著表示式呢?
也許總有一天會。在 Icon 中,return expr 既表示“我完成了”,也表示“但我還有一個最終有用的值要返回,就是這個”。在開始時,並且在沒有令人信服地使用 return expr 的情況下,專門使用 yield 來傳遞值會更清晰。
BDFL 公告
問題
引入另一個新關鍵字(例如,gen 或 generator)來代替 def,或者以其他方式改變語法,以區分生成器函式和非生成器函式。
反對
在實踐中(你如何看待它們),生成器 就是 函式,但它們的特殊之處在於它們是可恢復的。它們如何設定的機制是一個相對次要的技術問題,引入一個新關鍵字會不必要地過分強調生成器啟動的機制(生成器生命中一個重要但很小的部分)。
贊成
實際上(你如何看待它們),生成器函式實際上是工廠函式,它們像魔法一樣生成生成器迭代器。在這方面,它們與非生成器函式截然不同,更像是建構函式而不是函式,因此重用 def 充其量是令人困惑的。函式體中隱藏的 yield 語句不足以警告語義差異如此之大。
BDFL
def 保持不變。雙方的論點都不能完全令人信服,所以我請教了我的語言設計師直覺。它告訴我,PEP中提出的語法是完全正確的——既不過熱也不過冷。但是,就像希臘神話中的德爾斐神諭一樣,它沒有告訴我原因,所以我無法反駁針對PEP語法的論點。我能想到的最好的(除了同意……已經提出的反駁之外)是“FUD”。如果這從第一天起就是語言的一部分,我非常懷疑它會出現在 Andrew Kuchling 的“Python 糟點”頁面上。
參考實現
目前的實現,處於初步狀態(無文件,但經過充分測試且穩定),是 Python CVS 開發樹的一部分 [5]。使用它需要您從原始碼構建 Python。
這源自 Neil Schemenauer 早期的一個補丁 [4]。
腳註和參考文獻
版權
本文件已置於公共領域。
來源:https://github.com/python/peps/blob/main/peps/pep-0255.rst