python迭代器與生成器小結

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

迭代器(iterator)

要說生成器,必須首先說迭代器python3.x

區分iterable,iterator與itertion

講到迭代器,就須要區別幾個概念: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和listiterable 但不是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

這樣,由這幾個例子能夠解釋清楚這幾個概念的區別。

自定義iterator 與數據分離

說到這裏,迭代器對象基本出來了。下面大體說一下,如何讓自定義的類的對象成爲迭代器對象,其實就是定義__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從數據中分離出來,分別定義一個iterableiterator以下:

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的處理。

迭代器大概就記錄到這裏了,下面開始一個特殊的更加優雅的迭代器: 生成器

生成器(generator)

首先須要明確的就是生成器也是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,iteratoritertion的概念

  • 迭代器協議

    • 自定義可迭代對象與迭代器分離,保證數據複用

  • 生成器: 特殊的迭代器,內部實現了迭代器協議

其實這一塊, 那幾個概念搞清楚, ,這個很關鍵, 搞懂了後面就水到渠成了。並且對以前的知識也有不少加深。
好比常見list就是iteratoriteable分離實現的,自己是可迭代對象,但不是迭代器, 相似與xrange,可是又不一樣。
愈來愈明白,看源碼的重要性了。

參考

相關文章
相關標籤/搜索