[譯] PEP 255--簡單的生成器

圖片描述
我正打算寫寫 Python 的生成器,然而查資料時發現,引入生成器的 PEP 沒人翻譯過,所以就花了點時間翻譯出來。若是在閱讀時,你有讀不懂的地方,不用懷疑,極有多是我譯得不到位。若出現這種狀況,我建議你直接閱讀原文,最好也能將錯誤處告知於我,以便作出修改。html

原文https://www.python.org/dev/pe...node

建立日期:2001-05-18python

合入Python版本:2.2git

譯者豌豆花下貓Python貓 公衆號做者)github

PEP背景知識學習Python,怎能不懂點PEP呢?算法

摘要

這個 PEP 想在 Python 中引入生成器的概念,以及一個新的表達式,即 yield 表達式。express

動機

當一個生產者函數在處理某些艱難的任務時,它可能須要維持住生產完某個值時的狀態,大多數編程語言都提供不了既舒服又高效的方案,除了往參數列表中添加回調函數,而後每生產一個值時就去調用一下。編程

例如,標準庫中的tokenize.py採用這種方法:調用者必須傳一個 tokeneater 函數給 tokenize() ,當 tokenize() 找到下一個 token 時再調用。這使得 tokenize 能以天然的方式編碼,但程序調用 tokenize 會變得極其複雜,由於它須要記住每次回調前最後出現的是哪一個 token(s)。tabnanny.py中的 tokeneater 函數是處理得比較好的例子,它在全局變量中維護了一個狀態機,用於記錄已出現的 token 和預期會出現的 token 。這很難正確地工做,並且也挺難讓人理解。不幸的是,它已是最標準的解決方法了。微信

有一個替代方案是一次性生成 Python 程序的所有解析,並存入超大列表中。這樣 tokenize 客戶端能夠用天然的方式,即便用局部變量和局部控制流(例如循環和嵌套的 if 語句),來跟蹤其狀態。然而這並不實用:程序會變得臃腫,所以不能在實現整個解析所需的內存上放置先驗限制;而有些 tokenize 客戶端僅僅想要查看某個特定的東西是否曾出現(例如,future 聲明,或者像 IDLE 作的那樣,只是首個縮進的聲明),所以解析整個程序就是嚴重地浪費時間。app

另外一個替代方案是把 tokenize 變爲一個迭代器【註釋1】,每次調用它的 next() 方法時再傳遞下一個 token。這對調用者來講很便利,就像前一方案把結果存入大列表同樣,同時沒有內存與「想要早點退出怎麼辦」的缺點。然而,這個方案也把 tokenize 的負擔轉化成記住 next() 的調用狀態,讀者只要瞄一眼 tokenize.tokenize_loop() ,就會意識到這是一件多麼可怕的苦差事。或者想象一下,用遞歸算法來生成普通樹結構的節點:若把它投射成一個迭代器框架實現,就須要手動地移除遞歸狀態並維護遍歷的狀態。

第四種選擇是在不一樣的線程中運行生產者和消費者。這容許二者以天然的方式維護其狀態,因此都會很舒服。實際上,Python 源代碼發行版中的 Demo/threads/Generator.py 就提供了一個可用的同步通訊(synchronized-communication)類,來完成通常的任務。可是,這在沒有線程的平臺上沒法運用,並且就算可用也會很慢(與不用線程可取得的成就相比)。

最後一個選擇是使用 Python 的變種 Stackless 【註釋2-3】來實現,它支持輕量級的協程。它與前述的線程方案有相同的編程優點,效率還更高。然而,Stackless 在 Python 核心層存在爭議,Jython 也可能不會實現相同的語義。這個 PEP 不是討論這些問題的地方,但徹底能夠說生成器是 Stackless 相關功能的子集在當前 CPython 中的一種簡單實現,並且能夠說,其它 Python 實現起來也相對簡單。

以上分析完了已有的方案。其它一些高級語言也提供了不錯的解決方案,特別是 Sather 的迭代器,它受到 CLU 的迭代器啓發【註釋4】;Icon 的生成器,一種新穎的語言,其中每一個表達式都是生成器【註釋5】。它們雖有差別,但基本的思路是一致的:提供一種函數,它能夠返回中間結果(「下一個值」)給它的調用者,同時還保存了函數的局部狀態,以便在中止的位置恢復(譯註:resum,下文也譯做激活)調用。一個很是簡單的例子:

def fib():
    a, b = 0, 1
    while 1:
       yield b
       a, b = b, a+b

當 fib() 首次被調用時,它將 a 設爲 0,將 b 設爲 1,而後生成 b 給其調用者。調用者獲得 1。當 fib 恢復時,從它的角度來看,yield 語句實際上跟 print 語句相同:fib 繼續執行,且全部局部狀態無缺無損。而後,a 和 b 的值變爲 1,而且 fib 再次循環到 yield,生成 1 給它的調用者。以此類推。 從 fib 的角度來看,它只是提供一系列結果,就像用了回調同樣。可是從調用者的角度來看,fib 的調用就是一個可隨時恢復的可迭代對象。跟線程同樣,這容許兩邊以最天然的方式進行編碼;但與線程方法不一樣,這能夠在全部平臺上高效完成。事實上,恢復生成器應該不比函數調用昂貴。

一樣的方法適用於許多生產者/消費者函數。例如,tokenize.py 能夠生成下一個 token 而不是用它做爲參數調用回調函數,並且 tokenize 客戶端能夠以天然的方式迭代 tokens:Python 生成器是一種迭代器,可是特別強大。

設計規格:yield

引入了一種新的表達式:

yield_stmt:「yield」expression_list

yield 是一個新的關鍵字,所以須要一個 future 聲明【註釋8】來進行引入:在早期版本中,若想使用生成器的模塊,必須在接近頭部處包含如下行(詳見 PEP 236):

from __future__ import generators

沒有引入 future 模塊就使用 yield 關鍵字,將會告警。 在後續的版本中,yield 將是一個語言關鍵字,再也不須要 future 語句。

yield 語句只能在函數內部使用。包含 yield 語句的函數被稱爲生成器函數。從各方面來看,生成器函數都只是個普通函數,但在它的代碼對象的 co_flags 中設置了新的「CO_GENERATOR」標誌。

當調用生成器函數時,實際參數仍是綁定到函數的局部變量空間,但不會執行代碼。獲得的是一個 generator-iterator 對象;這符合迭代器協議【註釋6】,所以可用於 for 循環。注意,在上下文無歧義的狀況下,非限定名稱 「generator」 既能夠指生成器函數,又能夠指生成器-迭代器(generator-iterator)。

每次調用 generator-iterator 的 next() 方法時,纔會執行 generator-function 體中的代碼,直至遇到 yield 或 return 語句(見下文),或者直接迭代到盡頭。

若是執行到 yield 語句,則函數的狀態會被凍結,並將 expression_list 的值返回給 next() 的調用者。「凍結」是指掛起全部本地狀態,包括局部變量、指令指針和內部堆棧:保存足夠的信息,以便在下次調用 next() 時,函數能夠繼續執行,彷彿 yield 語句只是一次普通的外部調用。

限制:yield 語句不能用於 try-finally 結構的 try 子句中。困難的是不能保證生成器會被再次激活(resum),所以沒法保證 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 語句時,程序會正常 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/Exception/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]
>>>

示例

# 二叉樹類
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)

# 從列表中建立 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:]))

# 遞歸生成器,按順序生成樹標籤
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x
        yield t.label
        for x in inorder(t.right):
            yield x

# 展現:建立一棵樹
t = tree("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
# 按順序打印樹的節點
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

# 練習非遞歸生成器
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 而不用新的關鍵字?

請參閱下面的 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,而不強制用StopIteration?

「StopIteration」的機制是底層細節,就像 Python 2.1 中的「IndexError」的機制同樣:實現時須要作一些預先定義好的東西,而 Python 爲高級用戶開放了這些機制。儘管不強制要求每一個人都在這個層級工做。 「return」在任何一種函數中都意味着「我已經完成」,這很容易解讀和使用。注意,return 並不老是等同於 try-except 結構中的 raise StopIteration(參見「設計規格:Return」部分)。

那爲何不容許return一個表達式?

也許有一天會容許。 在 Icon 中,return expr 意味着「我已經完成」和「但我還有最後一個有用的值能夠返回,這就是它」。 在初始階段,不強制使用return expr的狀況下,使用 yield 僅僅傳遞值,這很簡單明瞭。

BDFL聲明

Issue

引入另外一個新的關鍵字(好比,gen 或 generator )來代替 def ,或以其它方式改變語法,以區分生成器函數和非生成器函數。

Con

實際上(你如何看待它們),生成器函數,但它們具備可恢復性。使它們創建起來的機制是一個相對較小的技術問題,引入新的關鍵字無助於強調生成器是如何啓動的機制(生成器生命中相當重要卻很小的部分)。

Pro

實際上(你如何看待它們),生成器函數其實是工廠函數,它們就像施了魔法同樣地生產生成器-迭代器。 在這方面,它們與非生成器函數徹底不一樣,更像是構造函數而不是函數,所以重用 def 無疑是使人困惑的。藏在內部的 yield 語句不足以警示它們的語義是如此不一樣。

BDFL

def 留了下來。任何一方都沒有任何爭論是徹底使人信服的,因此我諮詢了個人語言設計師的直覺。它告訴我 PEP 中提出的語法是徹底正確的——不是太熱,也不是太冷。可是,就像希臘神話中的 Delphi(譯註:特爾斐,希臘古都) 的甲骨文同樣,它並無告訴我緣由,因此我沒有對反對此 PEP 語法的論點進行反駁。 我能想出的最好的(除了已經贊成作出的反駁)是「FUD」(譯註:縮寫自 fear、uncertainty 和 doubt)。 若是這從第一天開始就是語言的一部分,我很是懷疑這早已讓安德魯·庫奇林(Andrew Kuchling)的「Python Warts」頁面成爲可能。(譯註:wart 是疣,一種難看的皮膚病。這是一個 wiki 頁面,列舉了對 Python 吹毛求疵的建議)。

參考實現

當前的實現(譯註:2001年),處於初步狀態(沒有文檔,但通過充分測試,可靠),是Python 的 CVS 開發樹【註釋9】的一部分。 使用它須要您從源代碼中構建 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.

版權信息

本文檔已經放置在公共領域。源文檔:https://github.com/python/peps/blob/master/pep-0255.txt

(譯文完)

PS:官方 PEP 有將近500個,然而保守估計,被翻譯成中文的不足20個(去重的狀況下)。我好奇,感興趣將一些重要的 PEP 翻譯出來的人有多少呢?現拋此問題出來探探路,歡迎留言交流。

-----------------

本文翻譯並首發於微信公衆號【Python貓】,後臺回覆「愛學習」,免費得到20+本精選電子書。

相關文章
相關標籤/搜索