Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案 (Python Enhancement Proposals)

PEP 255 – 簡單產生器

作者:
Neil Schemenauer <nas at arctrix.com>, Tim Peters <tim.peters at gmail.com>, Magnus Lie Hetland <magnus at hetland.org>
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
依賴項目:
234
建立日期:
2001年5月18日
Python 版本:
2.2
公告歷史:
2001年6月14日,2001年6月23日

目錄

摘要

本 PEP 將產生器 (Generators) 的概念引入 Python,並同時引入了一個與其搭配使用的新陳述式:yield 陳述式。

動機

當一個產生資料的函式工作過於繁重,需要保存產生數值之間的狀態時,大多數程式語言除了將回呼函式 (callback function) 加入產生器的參數列表,並在每次產生數值時呼叫該函式外,並沒有更優雅且高效的解決方案。

例如,標準函式庫中的 tokenize.py 就是採用這種方法:呼叫者必須傳遞一個 tokeneater 函式給 tokenize(),每當 tokenize() 找到下一個 token 時就會呼叫它。這讓 tokenize 可以用自然的方式編寫,但呼叫 tokenize 的程式通常會因為需要記住回呼之間上次看到的是哪些 token 而變得複雜。例如 tabnanny.py 中的 tokeneater 函式,它透過全域變數維護狀態機,以在回呼之間記錄已讀取內容以及預期下一個要讀取的內容。這很難正確實作,且對使用者來說也很難理解。不幸的是,這是這類方法的通病。

另一種替代方案是讓 tokenize 一次產出 Python 程式的完整剖析結果(放在一個大列表中)。這樣 tokenize 的客戶端就可以使用區域變數和區域控制流(如迴圈和巢狀 if 陳述式)以自然的方式編寫,並追蹤其狀態。但這並不實際:程式可能非常大,因此無法預先限制記憶體需求以儲存整個剖析結果;此外,有些 tokenize 客戶端只希望確認某個特定內容是否出現在程式開頭(例如 future 陳述式,或者像 IDLE 那樣,只需確認第一個縮排陳述式),那麼先剖析整個程式將會嚴重浪費時間。

另一種替代方案是將 tokenize 變為一個 迭代器 (iterator),在呼叫其 .next() 方法時傳回下一個 token。這對呼叫者來說與使用大列表一樣方便,且沒有記憶體消耗以及「如果我想提早跳出該怎麼辦?」的缺點。然而,這將維護 .next() 呼叫之間狀態的負擔轉移到了 tokenize 身上,讀者只需瀏覽一下 tokenize.tokenize_loop() 就能發現這是一項多麼可怕的工作。或者試想一個用於產出通用樹狀結構節點的遞迴演算法:若要將其轉換為迭代器架構,則需要手動移除遞迴並人工維護遍歷狀態。

第四種選擇是在獨立的執行緒 (threads) 中執行產生者和消費者。這允許兩者以自然的方式維護狀態,因此對雙方都很方便。實際上,Python 發行版本中的 Demo/threads/Generator.py 提供了一個可用的同步通訊類別來以通用方式實現這一點。然而,這在沒有執行緒的平台上無法運作,且在支援執行緒的平台上(相較於不使用執行緒能達到的效果)非常緩慢。

最後一個選擇是改用 Stackless [1] (PEP 219) 變體實作的 Python,它支援輕量級協同程序 (coroutines)。這在程式設計上有與執行緒選項大致相同的優點,且效率高得多。然而,Stackless 是對 Python 核心的一種具爭議性的重構,且 Jython 可能無法實作相同的語義。本 PEP 不是討論該問題的地方,因此在此僅說明:產生器以一種易於融入現有 CPython 實作的方式提供了 Stackless 功能的一個有用子集,並且被認為對於其他 Python 實作來說相對簡單。

目前的替代方案就這些。其他一些高階語言提供了不錯的解決方案,特別是 Sather [2] 中的迭代器(受 CLU 中的迭代器啟發),以及 Icon [3] 中的產生器(這是一種將每個表達式都視為產生器的新穎語言)。這些方案之間存在差異,但基本概念相同:提供一種可以向呼叫者傳回中間結果(「下一個值」)的函式,同時維護函式的區域狀態,以便該函式可以從上次中斷的地方繼續執行。一個非常簡單的例子

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 之後繼續執行,且所有區域狀態保持不變。接著 ab 變為 1 和 1,fib 迴圈回到 yield,向呼叫者 yield 出 1。依此類推。從 fib 的角度來看,它只是像透過回呼那樣傳遞結果序列。但從呼叫者的角度來看,fib 的呼叫是一個可以隨意恢復的迭代物件。就像執行緒方法一樣,這允許雙方以最自然的方式編寫程式;但與執行緒方法不同的是,這可以在所有平台上高效地完成。實際上,恢復一個產生器的開銷不應超過一個函式呼叫。

同樣的方法適用於許多生產者/消費者函式。例如,tokenize.py 可以 yield 出下一個 token,而不是將其作為參數呼叫回呼函式,而 tokenize 的客戶端可以以自然的方式迭代這些 token: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 旗標。

當呼叫產生器函式時,實際參數會以通常的方式綁定到函式區域的形式參數名稱上,但不會執行函式體內的任何程式碼。相反地,會傳回一個產生器-迭代器 (generator-iterator) 物件;這符合 迭代器協定,因此特別是可以自然地用於 for 迴圈中。請注意,當從上下文可以清楚理解意圖時,可使用不加限定的名稱「產生器」來指代產生器函式或產生器迭代器。

每當呼叫產生器迭代器的 .next() 方法時,產生器函式體內的程式碼就會執行,直到遇到 yieldreturn 陳述式(見下文),或者直到函式體結束為止。

如果遇到 yield 陳述式,函式的狀態就會凍結,並將 expression_list 的值傳回給 .next() 的呼叫者。「凍結」的意思是保留所有區域狀態,包括區域變數的目前綁定、指令指標以及內部評估堆疊:儲存足夠的資訊,以便下次呼叫 .next() 時,函式可以完全像 yield 陳述式只是另一個外部呼叫一樣繼續執行。

限制:yield 陳述式不允許出現在 try/finally 結構的 try 區塊中。困難在於,無法保證產生器一定會被恢復執行,因此無法保證 finally 區塊一定會被執行;這對 finally 的目的來說違反得太嚴重了。

限制:產生器在活躍執行狀態下不能被恢復 (resumed)

>>> 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 陳述式時,控制流程會像在任何函式 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

問與答 (Q & A)

為什麼不使用新關鍵字,而要重複使用 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 宣告

議題

引入另一個新關鍵字(例如 gengenerator)來取代 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

最後修改:2025年1月31日 10:51:19 GMT