轉載自:http://codingpy.com/article/python-generator-notes-by-kissg/
正文
要理解generator
,我們先從迭代(iteration)
與迭代器(iterator)
講起.當然,本文的重點是generator
,iteration
與iterator
的知識將點到即止。直接看generator
迭代是重複反饋過程的活動,其目的通常是爲了接近併到達所需的目標或結果。每一次對過程的重複被稱爲一次“迭代”,而每一次迭代得到的結果會被用來作爲下一次迭代的初始值。
以上是維基百科對迭代的定義。在python中,迭代通常是通過for ... in ...
來完成的,而且只要是可迭代對象(iterable)
,都能進行迭代。這裏簡單講下iterable
與iterator
:
iterable
是實現了__iter__()
方法的對象.更確切的說,是container.__iter__()
方法,該方法返回的是的一個iterator
對象,因此iterable
是你可以從其獲得iterator
的對象.~~使用iterable
時,將一次性返回所有結果,都存放在內存中,並且這些值都能重複使用.~~以上說法嚴重錯誤!對於iterable
,我們該關注的是,它是一個能一次返回一個成員的對象(iterable is an object capable of returning its members one at a time),一些iterable
將所有值都存儲在內存中,比如list
,而另一些並不是這樣,比如我們下面將講到的iterator
.
iterator
是實現了iterator.__iter__()
和iterator.__next__()
方法的對象.iterator.__iter__()
方法返回的是iterator
對象本身.根據官方的說法,正是這個方法,實現了for ... in ...
語句.而iterator.__next__()
是iterator
區別於iterable
的關鍵了,它允許我們顯式地獲取一個元素.當調用next()
方法時,實際上產生了2個操作:
- 更新
iterator
狀態,令其指向後一項,以便下一次調用 - 返回當前結果
如果你學過C++
,它其實跟指針的概念很像(如果你還學過鏈表的話,或許能更好地理解)。
正是__next__()
,使得iterator
能在每次被調用時,返回一個單一的值(有些教程裏,稱爲一邊循環,一邊計算,我覺得這個說法不是太準確。但如果這樣的說法有助於你的理解,我建議你就這樣記),從而極大的節省了內存資源。另一點需要格外注意的是,iterator
是消耗型的,即每一個值被使用過後,就消失了。因此,你可以將以上的操作2理解成pop
。對iterator
進行遍歷之後,其就變成了一個空的容器了,但不等於None
哦。因此,若要重複使用iterator
,利用list()
方法將其結果保存起來是一個不錯的選擇。
我們通過代碼來感受一下。
>>> from collections import Iterable, Iterator >>> a = [1,2,3] # 衆所周知,list是一個iterable >>> b = iter(a) # 通過iter()方法,得到iterator,iter()實際上調用了__iter__(),此後不再多說 >>> isinstance(a, Iterable) True >>> isinstance(a, Iterator) False >>> isinstance(b, Iterable) True >>> isinstance(b, Iterator) True # 可見,iterator是iterable,但iterable不一定是iterator # iterator是消耗型的,用一次少一次.對iterator進行變量,iterator就空了! >>> c = list(b) >>> c [1, 2, 3] >>> d = list(b) >>> d [] # 空的iterator並不等於None. >>> if b: ... print(1) ... 1 >>> if b == None: ... print(1) ... # 再來感受一下next() >>> e = iter(a) >>> next(e) #next()實際調用了__next__()方法,此後不再多說 1 >>> next(e) 2
既然提到了for ... in ...
語句,我們再來簡單講下其工作原理吧,或許能幫助理解以上所講的內容。
>>> x = [1, 2, 3] >>> for i in x: ... ...
我們對一個iterable
用for ... in ...
進行迭代時,實際是先通過調用iter()
方法得到一個iterator
,假設叫做X。然後循環地調用X的next()
方法取得每一次的值,直到iterator爲空,返回的StopIteration
作爲循環結束的標誌。for ... in ...
會自動處理StopIteration
異常,從而避免了拋出異常而使程序中斷。如圖所示
磨刀不誤砍柴工,有了前面的知識,我們再來理解generator
與yield
將會事半功倍。
首先先理清幾個概念:
generator
: A function which returns a generator iterator. It looks like a normal function except that it contains yield expressions for producing a series of values usable in a for-loop or that can be retrieved one at a time with the next() function.
generator iterator
: An object created by a generator funcion.
generator expression
: An expression that returns an iterator.
以上的定義均來自python官方文檔。可見,我們常說的生成器
,就是帶有yield
的函數,而generator iterator
則是generator function
的返回值,即一個generator
對象,而形如(elem for elem in [1, 2, 3])
的表達式,稱爲generator expression
,實際使用與generator
無異。
>>> a = (elem for elem in [1, 2, 3]) >>> a <generator object <genexpr> at 0x7f0d23888048> >>> def fib(): ... a, b = 0, 1 ... while True: ... yield b ... a, b = b, a + b ... >>> fib <function fib at 0x7f0d238796a8> >>> b = fib() <generator object fib at 0x7f0d20bbfea0>
其實說白了,generator
就是iterator
的一種,以更優雅的方式實現的iterator
。官方的說法是:
Python’s generators provide a convenient way to implement the iterator protocol.
你完全可以像使用iterator
一樣使用generator
,當然除了定義。定義一個iterator
,你需要分別實現__iter__()
方法和__next__()
方法,但generator
只需要一個小小的yield
(好吧,generator expression
的使用比較簡單,就不展開講了。)
前文講到iterator
通過__next__()
方法實現了每次調用,返回一個單一值的功能。而yield
就是實現generator
的__next__()
方法的關鍵!先來看一個最簡單的例子:
>>> def g(): ... print("1 is") ... yield 1 ... print("2 is") ... yield 2 ... print("3 is") ... yield 3 ... >>> z = g() >>> z <generator object g at 0x7f0d2387c8b8> >>> next(z) 1 is 1 >>> next(z) 2 is 2 >>> next(z) 3 is 3 >>> next(z) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
第一次調用next()
方法時,函數似乎執行到yield 1
,就暫停了。然後再次調用next()
時,函數從yield 1
之後開始執行的,並再次暫停。第三次調用next()
,從第二次暫停的地方開始執行。第四次,拋出StopIteration
異常。
事實上,generator
確實在遇到yield
之後暫停了,確切點說,是先返回了yield
表達式的值,再暫停的。當再次調用next()
時,從先前暫停的地方開始執行,直到遇到下一個yield
。這與上文介紹的對iterator
調用next()
方法,執行原理一般無二。
有些教程裏說generator
保存的是算法,而我覺得用中斷服務子程序
來描述generator
或許能更好理解,這樣你就能將yield
理解成一箇中斷服務子程序的斷點
,沒錯,是中斷服務子程序的斷點。我們每次對一個generator
對象調用next()
時,函數內部代碼執行到"斷點"yield
,然後返回這一部分的結果,並保存上下文環境,"中斷"返回。
怎麼樣,是不是瞬間就明白了yield
的用法?
我們再來看另一段代碼。
>>> def gen(): ... while True: ... s = yield ... print(s) ... >>> g = gen() >>> g.send("kissg") Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't send non-None value to a just-started generator >>> next(g) >>> g.send("kissg") kissg
我正是看到這個形式的generator
,懵了,纔想要深入學習generator
與yield
的。結合以上的知識,我再告訴你,generator
其實有第2種調用方法(恢復執行),即通過send(value)
方法將value
作爲yield
表達式的當前值,你可以用該值再對其他變量進行賦值,這一段代碼就很好理解了。當我們調用send(value)
方法時,generator
正由於yield
的緣故被暫停了。此時,send(value)
方法傳入的值作爲yield
表達式的值,函數中又將該值賦給了變量s
,然後print函數打印s
,循環再遇到yield
,暫停返回。
調用send(value)
時要注意,要確保,generator
是在yield
處被暫停了,如此才能向yield
表達式傳值,否則將會報錯(如上所示),可通過next()
方法或send(None)
使generator
執行到yield
。
再來看一段yield
更復雜的用法,或許能加深你對generator
的next()
與send(value)
的理解。
>>> def echo(value=None): ... while 1: ... value = (yield value) ... print("The value is", value) ... if value: ... value += 1 ... >>> g = echo(1) >>> next(g) 1 >>> g.send(2) The value is 2 3 >>> g.send(5) The value is 5 6 >>> next(g) The value is None
上述代碼既有yield value
的形式,又有value = yield
形式,看起來有點複雜。但以yield
分離代碼進行解讀,就不太難了。第一次調用next()
方法,執行到yield value
表達式,保存上下文環境暫停返回1
。第二次調用send(value)
方法,從value = yield
開始,打印,再次遇到yield value
暫停返回。後續的調用send(value)
或next()
都不外如是。
但是,這裏就引出了另一個問題,yield
作爲一個暫停恢復的點,代碼從yield
處恢復,又在下一個yield
處暫停。可見,在一次next()
(非首次)或send(value)
調用過程中,實際上存在2個yield
,一個作爲恢復點的yield
與一個作爲暫停點的yield
。因此,也就有2個yield
表達式。send(value)
方法是將值傳給恢復點yield
;調用next()
表達式的值時,其恢復點yield
的值總是爲None
,而將暫停點的yield
表達式的值返回。爲方便記憶,你可以將此處的恢復點記作當前的(current),而將暫停點記作下一次的(next),這樣就與next()
方法匹配起來啦。
generator
還實現了另外兩個方法throw(type[, value[, traceback]])
與close()
。前者用於拋出異常,後者用於關閉generator
.不過這2個方法似乎很少被直接用到,本文就不再多說了,有興趣的同學請看這裏。
小結
可迭代對象(Iterable)是實現了
__iter__()
方法的對象,通過調用iter()
方法可以獲得一個迭代器(Iterator)。迭代器(Iterator)是實現了
__iter__()
和__next__()
的對象。for ... in ...
的迭代,實際是將可迭代對象轉換成迭代器,再重複調用next()
方法實現的。生成器(generator)是一個特殊的迭代器,它的實現更簡單優雅
yield
是生成器實現__next__()
方法的關鍵。它作爲生成器執行的暫停恢復點,可以對yield
表達式進行賦值,也可以將yield
表達式的值返回。