python學習之generator

轉載自:http://codingpy.com/article/python-generator-notes-by-kissg/  

正文

要理解generator,我們先從迭代(iteration)迭代器(iterator)講起.當然,本文的重點是generator,iterationiterator的知識將點到即止。直接看generator

迭代是重複反饋過程的活動,其目的通常是爲了接近併到達所需的目標或結果。每一次對過程的重複被稱爲一次“迭代”,而每一次迭代得到的結果會被用來作爲下一次迭代的初始值。

以上是維基百科對迭代的定義。在python中,迭代通常是通過for ... in ...來完成的,而且只要是可迭代對象(iterable),都能進行迭代。這裏簡單講下iterableiterator

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個操作:

  1. 更新iterator狀態,令其指向後一項,以便下一次調用
  2. 返回當前結果

如果你學過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:
...     ...

我們對一個iterablefor ... in ...進行迭代時,實際是先通過調用iter()方法得到一個iterator,假設叫做X。然後循環地調用X的next()方法取得每一次的值,直到iterator爲空,返回的StopIteration作爲循環結束的標誌。for ... in ...會自動處理StopIteration異常,從而避免了拋出異常而使程序中斷。如圖所示

what-really-happens-in-for-in-statement

磨刀不誤砍柴工,有了前面的知識,我們再來理解generatoryield將會事半功倍。

首先先理清幾個概念:

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,懵了,纔想要深入學習generatoryield的。結合以上的知識,我再告訴你,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更復雜的用法,或許能加深你對generatornext()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)調用過程中,實際上存在2yield,一個作爲恢復點的yield與一個作爲暫停點的yield。因此,也就有2個yield表達式。send(value)方法是將值傳給恢復點yield;調用next()表達式的值時,其恢復點yield的值總是爲None,而將暫停點yield表達式的值返回。爲方便記憶,你可以將此處的恢復點記作當前的(current),而將暫停點記作下一次的(next),這樣就與next()方法匹配起來啦。

generator還實現了另外兩個方法throw(type[, value[, traceback]])close()。前者用於拋出異常,後者用於關閉generator.不過這2個方法似乎很少被直接用到,本文就不再多說了,有興趣的同學請看這裏

小結

iterable-iterator-generator

  1. 可迭代對象(Iterable)是實現了__iter__()方法的對象,通過調用iter()方法可以獲得一個迭代器(Iterator)。

  2. 迭代器(Iterator)是實現了__iter__()__next__()的對象。

  3. for ... in ...的迭代,實際是將可迭代對象轉換成迭代器,再重複調用next()方法實現的。

  4. 生成器(generator)是一個特殊的迭代器,它的實現更簡單優雅

  5. yield是生成器實現__next__()方法的關鍵。它作爲生成器執行的暫停恢復點,可以對yield表達式進行賦值,也可以將yield表達式的值返回。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章