Python:生成器

  • 2020 年 1 月 10 日
  • 筆記

    生成器是Python中的一個高級用法,有段時間我對生成器的理解頗為費勁,直到我看到一句話「yield語句掛起該生成器函數的狀態,保留足夠的資訊,以便之後從它離開的地方繼續執行」後,讓我恍然大悟,這是生成器中的狀態掛起,這句話讓我想起了在大學時玩ARM單片機時經常碰到的一個概念——中斷,單片機在遇到中斷訊號時,處理中斷程式前也要先保護現場,即系統要在執行中斷程式之前,必須保存當前處理機程式狀態字PSW和程式計數器PC等的值,待中斷程式執行完成後在回復現場繼續執行下面的程式。仔細想想,個人覺得在保護「現場」這一點上,兩者中的道理還是差不多的(也許你並不這麼認同),有時候一個新概念的理解就是卡在一個小知識點上,我之前一直不明白「生成器掛起狀態」是什麼東西,但是回頭瞬間想起以前學過的知識,然後類比,有些東西也就恍然大悟了,也是這個「聯想」讓我對生成器有了更深刻的理解,使用起來也得心應手。現在工作當中,特別是在做數據統計時,碰到了特別長的列表時,我都是用生成器,不進可以節省記憶體,而且程式碼更加優雅。下面就來講講生成器,不正之處歡迎批評指正!

   生成器就是按照一定演算法生產的序列,也就是序列元素可以按照某種演算法推算出來,即在循環的過程中不斷推算出後續的元素,這樣就不必創建完整的序列,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱為生成器(Generator)。

(一)生成器語法

生成器表達式: 通列表解析語法,只不過把列表解析的[]換成()

生成器表達式能做的事情列表解析基本都能處理,只不過在需要處理的序列比較大時,列表解析比較費記憶體。

>>> L = [x * x for x in range(10)]  >>> L  [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]  >>> g = (x * x for x in range(10))  >>> g  <generator object <genexpr> at 0x104feab40>

L是一個list,而g是一個generator。如果要一個一個列印出來,可以通過generator的next()方法。每次調用next(),就計算出下一個元素的值,直到計算到最後一個元素,沒有更多的元素時,拋出StopIteration的錯誤。這裡就不過多闡述,大家可以在終端試試,不斷執行g.next(),同時可以用sys.getsizeof()來比較下L和g所用記憶體的大小,這裡列表元素比較少,看不出生成器的優勢,但是,對於g,把推到式中的range(10)改成range(100),range(100),g所佔記憶體是不會改變的,大家可以試試。

生成器函數: 在函數中如果出現了yield關鍵字,那麼該函數就不再是普通函數,而是生成器函數。

但是生成器函數可以生產一個無限的序列,這樣列表根本沒有辦法進行處理。yield 的作用就是把一個函數變成一個 generator,帶有 yield 的函數不再是一個普通函數,Python 解釋器會將其視為一個 generator。

def gensquares(N):      for i in range(N):          yield i ** 2             for item in gensquares(5):      print item

這是個簡單的例子,使用生成器返回自然數的平方。

(二)生成器的方法

我們可以用dir()函數來看看生成器對象的方法,如下:

['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']

它裡面有__iter__()和next()方法,這不就是迭代器協議要滿足的兩個基本條件嗎?(不了解迭代器協議,可以看之前的博文,點此)也就是說生成器是一個特殊的迭代器。

close()

手動關閉生成器函數,後面的調用會直接返回StopIteration異常。看下面簡單例子:

send()

生成器函數最大的特點是可以接受外部傳入的一個變數,並根據變數內容計算結果後返回。

這是生成器函數最難理解的地方,也是最重要的地方。

首先看個簡單的例子

#coding=utf-8  def fun(value=None):      print "begin"        while 1:          try:              value = (yield value)              print "yield"          except Exception,e:              value = e      g = fun(8)  print g.next()  print "==============="  print g.next()  print "==============="  print g.next()

運行結果如下:

    由上圖的運行結果可知,生成器函數調用後,它的函數體並沒有執行,而是到第一次調用next()時才開始執行,而且是執行到yield表達式為止,此時就要狀態掛起,第二次調用next()時再恢復之前的掛起狀態接著執行,所以第一次執行next()時,並沒有列印出"yield",到第二次調用next()時,第一個執行的就是print "yield"語句,所以也就列印出了"yield",直到再次遇到yield表達式,然後再掛起,依次類推。

這裡還要提到一點就是yield表達式,第一次調用next()時,value = yield v語句中只執行了yield v這個表達式,而賦值操作並未執行。只有第二次調用next()時yield表達式的值賦給了value,而yield表達式的默認「返回值」是None.

這一塊大家可以參考這篇博文

在函數里單獨的yield 5 與m = yield 5還是有區別的。

這可能有點難理解,舉個例子來驗證下:

#coding=utf-8  class A(object):      def __init__(self,v):          self._value = v      def fun(self,value):          print "begin"          while 1:              try:                  self._value = (yield value)                  print "aaa",self._value                  print "yield"              except Exception,e:                  self._value = e    G = A(8)  g = G.fun(88)  print "_value  " ,  G._value  print g.next()  print "_value  " ,G._value  print "==============="  print g.next()  print "_value  " ,G._value  print "==============="  print g.next()  print "_value  " ,G._value

運行結果如下:

從運行結果上來看,第一次調用next()時,G._value的值並沒有改變,說明此時self._value = (yield value)並沒有執行賦值操作,第二次調用next()時,G._value的值改變了,為None,說明執行了賦值操作。

有了上面的一些基礎,理解send()方法應該很容易,看下面例子:

#coding=utf-8  def fun(v):      while 1:            value = (yield v)          if value == 14:              break          v = 'get: %s' % value    g = fun(None)  print g.send(None)  print g.send(10)  print g.send(12)  print g.send(14)

執行流程:

1.通過g.send(None)或者next(g)可以啟動生成器函數,並執行到第一個yield語句結束的位置。

此時,執行完了yield語句,但是沒有給value賦值。注意:在啟動生成器函數時只能send(None),如果試圖輸入其它的值都會得到錯誤提示資訊。這裡,如果你去掉g.send(None)這句,就會報錯。

2.通過g.send(10),會傳入10,並賦值給value,然後計算出v的值,並回到while頭部,執行yield v語句有停止。此時會輸出"get: 10",然後掛起。

3.通過g.send(12),會重複第2步,最後輸出結果為"got:12"

4.當我們g.send(14)時,程式會執行break然後推出循環,最後整個函數執行完畢,所以會是StopIteration異常。

其實,send()是全功能版本的next(),next()相當於send(None),前面提到過yield表達式有「返回值」,send()作用就是控制這個「返回值」的,使得yield表達式的返回值是它的實參。

這一句要好好理解,看上面的例子,最後列印出來的值都是函數中v的值(也就是實參)。

throw()

用來向生成器函數送入一個異常,可以結束系統定義的異常,或者自定義的異常。

throw()後直接拋出異常並結束程式,或者消耗掉一個yield,或者在沒有下一個yield的時候直接進行到程式的結尾。

#coding=utf-8  def gen():      while True:          try:              yield 'normal value'              yield 'normal value 2'              print('here')          except ValueError:              print('we got ValueError here')          except TypeError:              break    g=gen()  print next(g)  print g.throw(ValueError)  print next(g)  print g.throw(TypeError)

1.print next(g):會輸出normal value,並停留在yield 'normal value 2'之前。

2.由於執行了g.throw(ValueError),所以會跳過所有後續的try語句,也就是說yield 'normal value 2'不會被執行,然後進入到except語句,列印出we got ValueError here。然後再次進入到while語句部分,消耗一個yield,所以會輸出normal value。然後狀態掛起。

3.print next(g),會執行yield 'normal value 2'語句,並停留在執行完該語句後的位置。

4.g.throw(TypeError):會跳出try語句,從而print('here')不會被執行,然後執行break語句,跳出while循環,然後到達程式結尾,所以跑出StopIteration異常。

最後運行結果如下:

生成器的主要三個方法中,send()方法是比較難理解的,不過只要記住send()作用就是控制yield表達式「返回值」的,使得yield表達式的返回值是它的實參。

最後總結起來就這麼幾句:

1.生成器就是一種迭代器,可以使用for進行迭代。

2.第一次執行next(generator)時,會執行完yield語句後程式進行掛起,所有的參數和狀態會進行保存。再一次執行next(generator)時,會從掛起的狀態開始往後執行。在遇到程式的結尾或者遇到StopIteration時,循環結束。

3.生成器函數和常規函數幾乎是一樣的。它們都是使用def語句進行定義,差別在於,生成器使用yield語句返回一個值,而常規函數使用return語句返回一個值

4.可以通過generator.send(arg)來傳入參數,這是協程模型。

5.可以通過generator.throw(exception)來傳入一個異常。throw語句會消耗掉一個yield。

6.可以通過generator.close()來手動關閉生成器。

7.next()等價於send(None)