Python:生成器

    生成器是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異常。看下面簡單例子:

wKiom1hLkvjBvTB5AABZKcZX7Ls839.png-wh_50

    

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()

運行結果如下:

wKioL1hLroeTDTlYAAAw15CTFsM013.png-wh_50

    由上圖的運行結果可知,生成器函數調用後,它的函數體並沒有執行,而是到第一次調用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

運行結果如下:

wKiom1hLtdnzXFJVAABIR1AiII8552.png-wh_50

從運行結果上來看,第一次調用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異常。

wKiom1hLwzTTePp4AABPlYMLy1A260.png-wh_50



其實,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異常。

最後運行結果如下:

wKiom1hLxxOjhC2SAABWgtuh7gQ091.png-wh_50


生成器的主要三個方法中,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)



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