當前位置:
首頁 > 知識 > PEP 255 :簡單的生成器

PEP 255 :簡單的生成器

(給

Python開發者

加星標,提升Python技能


作者:

豌豆花下貓

(本文來自作者投稿)


摘要


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


動機


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

例如,標準庫中的

tokenize.py

採用這種方法:調用者必須傳一個 tokeneater 函數給 tokenize() ,當 tokenize() 找到下一個 token 時再調用。這使得 tokenize 能以自然的方式編碼,但程序調用 tokenize 會變得極其複雜,因為它需要記住每次回調前最後出現的是哪個 token(s)。

tabnanny.py

中的 tokeneater 函數是處理得比較好的例子,它在全局變數中維護了一個狀態機,用於記錄已出現的 token 和預期會出現的 token 。這很難正確地工作,而且也挺難讓人理解。不幸的是,它已經是最標準的解決方法了。


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


另一個替代方案是把 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 + 

"
"

 + 

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

)

# 從列表中創建 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/peps/pep-0234/


[2] http://www.stackless.com/


[3] PEP-219, Stackless Python, McMillan


http://www.python.org/dev/peps/pep-0219/


[4] "Iteration Abstraction in Sather" Murer, Omohundro, Stoutamire and Szyperski 


http://www.icsi.berkeley.edu/~sather/Publications/toplas.html


[5] http://www.cs.arizona.edu/icon/


[6] The  concept  of  iterators  is  described  in PEP 234. 


[7] http://python.ca/nas/python/generator.diff


[8] PEP 236, Back to the 

future

, Peters


http://www.python.org/dev/peps/pep-0236/


[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


【本文作者】

豌豆花下貓:某985高校畢業生, 兼具極客思維與人文情懷 。個人公眾號Python貓, 專註python技術、數據科學和深度學習。




推薦閱讀


(點擊標題可跳轉閱讀)


完全理解 Python 迭代對象、迭代器、生成器


Python 生成器原理詳解


Python 迭代器和生成器

覺得本文對你有幫助?請分享給更多人


關注「Python開發者」加星標,提升Python技能



喜歡就點一下「好看」唄~

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發者 的精彩文章:

10 分鐘快速入門 Python3
Python 之父退位後,最高決策權花落誰家?

TAG:Python開發者 |