2016.3.10關於例子解釋的補充更新html
源自個人博客python
老規矩,先上一個代碼:git
def add(s, x): return s + x def gen(): for i in range(4): yield i base = gen() for n in [1, 10]: base = (add(i, n) for i in base) print list(base)
這個東西輸出能夠腦補一下, 結果是[20,21,22,23]
, 而不是[10, 11, 12, 13]
。 當時糾結了半天,一直沒搞懂,後來齊老師稍微指點了一下, 忽然想明白了--真夠笨的,唉。。好了--正好趁機會稍微小結一下python裏面的生成器。github
要說生成器,必須首先說迭代器python3.x
講到迭代器,就須要區別幾個概念:iterable
,iterator
,itertion
, 看着都差很少,其實否則。下面區分一下。數組
itertion
: 就是迭代
,一個接一個(one after another),是一個通用的概念,好比一個循環遍歷某個數組。app
iterable
: 這個是可迭代對象
,屬於python的名詞,範圍也很廣,可重複迭代,知足以下其中之一的都是iterable
:函數
能夠for
循環: for i in iterable
學習
能夠按index
索引的對象,也就是定義了__getitem__
方法,好比list,str
;code
定義了__iter__
方法。能夠隨意返回。
能夠調用iter(obj)
的對象,而且返回一個iterator
iterator
: 迭代器對象
,也屬於python的名詞,只能迭代一次。須要知足以下的迭代器協議
定義了__iter__
方法,可是必須返回自身
定義了next
方法,在python3.x是__next__
。用來返回下一個值,而且當沒有數據了,拋出StopIteration
能夠保持當前的狀態
首先str和list
是iterable
但不是iterator
:
In [3]: s = 'hi' In [4]: s.__getitem__ Out[4]: <method-wrapper '__getitem__' of str object at 0x7f9457eed580> In [5]: s.next # 沒有next方法 --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-5-136d3c11be25> in <module>() ----> 1 s.next AttributeError: 'str' object has no attribute 'next' In [6]: l = [1,2] # 同理 In [7]: l.__iter__ Out[7]: <method-wrapper '__iter__' of list object at 0x7f945328c320> In [8]: l.next --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-8-c6f8fb94c4cd> in <module>() ----> 1 l.next AttributeError: 'list' object has no attribute 'next' In [9]: iter(s) is s #iter() 沒有返回自己 Out[9]: False In [10]: iter(l) is l #同理 Out[10]: False
可是對於iterator
則不同以下, 另外iterable
能夠支持屢次迭代,而iterator
在屢次next
以後,再次調用就會拋異常,只能夠迭代一次。
In [13]: si = iter(s) In [14]: si Out[14]: <iterator at 0x7f9453279dd0> In [15]: si.__iter__ # 有__iter__ Out[15]: <method-wrapper '__iter__' of iterator object at 0x7f9453279dd0> In [16]: si.next #擁有next Out[16]: <method-wrapper 'next' of iterator object at 0x7f9453279dd0> In [20]: si.__iter__() is si #__iter__返回本身 Out[20]: True
這樣,由這幾個例子能夠解釋清楚這幾個概念的區別。
說到這裏,迭代器對象基本出來了。下面大體說一下,如何讓自定義的類的對象成爲迭代器對象,其實就是定義__iter__
和next
方法:
In [1]: %paste class DataIter(object): def __init__(self, *args): self.data = list(args) self.ind = 0 def __iter__(self): #返回自身 return self def next(self): # 返回數據 if self.ind == len(self.data): raise StopIteration else: data = self.data[self.ind] self.ind += 1 return data ## -- End pasted text -- In [9]: d = DataIter(1,2) In [10]: for x in d: # 開始迭代 ....: print x ....: 1 2 In [13]: d.next() # 只能迭代一次,再次使用則會拋異常 --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) ----> 1 d.next() <ipython-input-1-c44abc1904d8> in next(self) 10 def next(self): 11 if self.ind == len(self.data): ---> 12 raise StopIteration 13 else: 14 data = self.data[self.ind]
從next
函數中只能向前取數據,一次取一個能夠看出來,不過不能重複取數據,那這個可不能夠解決呢?
咱們知道iterator
只能迭代一次,可是iterable
對象則沒有這個限制,所以咱們能夠把iterator
從數據中分離出來,分別定義一個iterable
與iterator
以下:
class Data(object): # 只是iterable:可迭代對象而不iterator:迭代器 def __init__(self, *args): self.data = list(args) def __iter__(self): # 並無返回自身 return DataIterator(self) class DataIterator(object): # iterator: 迭代器 def __init__(self, data): self.data = data.data self.ind = 0 def __iter__(self): return self def next(self): if self.ind == len(self.data): raise StopIteration else: data = self.data[self.ind] self.ind += 1 return data if __name__ == '__main__': d = Data(1, 2, 3) for x in d: print x, for x in d: print x,
輸出就是:
1,2,3 1,2,3
能夠看出來數據能夠複用,由於每次都返回一個DataIterator
,可是數據卻能夠這樣使用,這種實現方式很常見,好比xrange
的實現即是這種數據與迭代分離的形式,可是很節省內存,以下:
In [8]: sys.getsizeof(range(1000000)) Out[8]: 8000072 In [9]: sys.getsizeof(xrange(1000000)) Out[9]: 40
另外有個小tips, 就是爲何可使用for 迭代迭代器對象,緣由就是for
替咱們作了next
的活,以及接收StopIteration
的處理。
迭代器大概就記錄到這裏了,下面開始一個特殊的更加優雅的迭代器: 生成器
首先須要明確的就是生成器也是
iterator
迭代器,由於它遵循了迭代器協議.
yield
的函數生成器函數跟普通函數只有一點不同,就是把 return
換成yield
,其中yield
是一個語法糖,內部實現了迭代器協議,同時保持狀態能夠掛起。以下:
記住一點,yield
是數據的生產者,而諸如for
等是數據的消費者。
def gen(): print 'begin: generator' i = 0 while True: print 'before return ', i yield i i += 1 print 'after return ', i a = gen() In [10]: a #只是返回一個對象 Out[10]: <generator object gen at 0x7f40c33adfa0> In [11]: a.next() #開始執行 begin: generator before return 0 Out[11]: 0 In [12]: a.next() after return 1 before return 1 Out[12]: 1
首先看到while True
沒必要驚慌,它只會一個一個的執行~
看結果能夠看出一點東西:
調用gen()
並無真實執行函數,而是隻是返回了一個生成器對象
執行第一次a.next()
時,才真正執行函數,執行到yield
一個返回值,而後就會掛起,保持當前的名字空間等狀態。而後等待下一次的調用,從yield
的下一行繼續執行。
還有一種狀況也會執行生成器函數,就是當檢索生成器的元素時,如list(generator)
, 說白了就是當須要數據的時候,纔會執行。
In [15]: def func(): ....: print 'begin' ....: for i in range(4): ....: yield i In [16]: a = func() In [17]: list(a) #檢索數據,開始執行 begin Out[17]: [0, 1, 2, 3]
yield
還有其餘高級應用,後面再慢慢學習。
列表生成器十分方便:以下,求10之內的奇數:[i for i in range(10) if i % 2]
一樣在python 2.4
也引入了生成器表達式
,並且形式很是相似,就是把[]
換成了()
.
In [18]: a = ( i for i in range(4)) In [19]: a Out[19]: <generator object <genexpr> at 0x7f40c2cfe410> In [20]: a.next() Out[20]: 0
能夠看出生成器表達式建立了一個生成器,並且生有個特色就是惰性計算
, 只有在被檢索時候,纔會被賦值。
以前有篇文章:python 默認參數問題及一個應用,最後有一個例子:
def multipliers(): return (lambda x : i * x for i in range(4)) #修改爲生成器 print [m(2) for m in multipliers()]
這個就是說,只有在執行m(2)
的時候,生成器表達式裏面的for
纔會開始從0循環,而後接着纔是i * x
,所以不存在那篇文章中的問題.
惰性計算這個特色頗有用,上述就是一個應用,2gua這樣說的:
惰性計算想像成水龍頭,須要的時候打開,接完水了關掉,這時候數據流就暫停了,再須要的時候再打開水龍頭,這時候數據還是接着輸出,不須要從頭開始循環
我的理解就是就是能夠利用生成器來做爲數據管道使用,當被檢索的時候,每次拿出一個數據,而後向下面傳遞,傳到最後,再拿第二個數據,在下面的例子中會詳細說明。
其實本質跟迭代器差很少,不一次性把數據都那過來,須要的時候,纔拿。
看到這裏,開始的例子應該大概能夠有點清晰了,
核心語句就是:
def gen(): for i in range(4): yield i for n in [1, 10]: base = (add(i, n) for i in base)
以前的解釋有點瑕疵,容易誤導對生成器的理解:
在執行list(base)
的時候,開始檢索,而後生成器開始運算了。關鍵是,這個循環次數是2,也就是說,有兩次生成器表達式的過程。必須緊緊把握住這一點。生成器返回去開始運算,n = 10
而不是1沒問題吧,這個在上面提到的文章中已經提到了,就是add(i+n)綁定的是n
這個變量,而不是它當時的數值。而後首先是第一次生成器表達式的執行過程:base = (10 + 0, 10 + 1, 10 + 2, 10 +3)
,這是第一次循環的結果(形象表示,其實已經計算出來了(10,11,12,3)),而後第二次,base = (10 + 10, 11 + 10, 12 + 10, 13 + 10)
,終於獲得結果了[20, 21, 22, 23]
.
新思路
這個能夠以管道的思路來理解,首先gen()
函數是第一個生成器,下一個是第一次循環的base = (add(i, n) for i in base)
,最後一個生成器是第二次循環的base = (add(i, n) for i in base)
。
這樣就至關於三個管道依次鏈接,可是水(數據)尚未流過,如今到了list(base)
,就至關於驅動器,打開了水的開關,這時候,按照管道的順序,由第一個產生一個數據,yield 0
,而後第一個管道關閉。
以後傳遞給第二個管道就是第一次循環,此時執行了add(0, 10)
,而後水繼續流,到第二次循環,再執行add(10, 10)
,此時到管道尾巴了,此時產生了第一個數據20,而後第一個管道再開放:yield 1
, 流程跟上面的同樣,依次產生21,22,23; 直到沒有數據。
把代碼改一下容易理解:
def gen(): for i in range(4): yield i # 第一個管道 base = (add(i, 10) for i in base) # 第二個管道 base = (add(i, 10) for i in base) # 第三個管道 list(base) # 開關驅動器
具體執行過程能夠在pythontutor上:
以前的解釋被誤導的緣由是,可能會誤覺得是在第二個管道就把gen()
執行完畢了,其實不是這樣的。
這種寫法的好處顯而易見:內存佔用低。在數據量極大的時候,用list
就只能爆內存,而用生成器模式則徹底不用擔憂
主要介紹了大概這樣幾點:
iterable
,iterator
與itertion
的概念
迭代器協議
自定義可迭代對象與迭代器分離,保證數據複用
生成器: 特殊的迭代器,內部實現了迭代器協議
其實這一塊, 那幾個概念搞清楚, ,這個很關鍵, 搞懂了後面就水到渠成了。並且對以前的知識也有不少加深。
好比常見list
就是iterator
與iteable
分離實現的,自己是可迭代對象,但不是迭代器, 相似與xrange
,可是又不一樣。
愈來愈明白,看源碼的重要性了。