Py西游攻關之迭代器&生成器


一 概要

在了解Python的數據結構時,容器(container)、可迭代對象(iterable)、迭代器(iterator)、生成器(generator)、列表/集合/字典推導式(list,set,dict comprehension)眾多概念參雜在一起,難免讓初學者一頭霧水,我將用一篇文章試圖將這些概念以及它們之間的關系捋清楚

 

二 容器(container)

容器是一種把多個元素組織在一起的數據結構,容器中的元素可以逐個地迭代獲取,可以用 in , not in 關鍵字判斷元素是否包含在容器中。通常這類數據結構把所有的元素存儲在內存中(也有一些特列並不是所有的元素都放在內存)在Python中,常見的容器對象有:

  • list, deque, ....
  • set, frozensets, ....
  • dict, defaultdict, OrderedDict, Counter, ....
  • tuple, namedtuple, …
  • str

容器比較容易理解,因為你就可以把它看作是一個盒子、一棟房子、一個櫃子,里面可以塞任何東西。從技術角度來說,當它可以用來詢問某個元素是否包含在其中時,那么這個對象就可以認為是一個容器,比如 list,set,tuples都是容器對象:

盡管絕大多數容器都提供了某種方式來獲取其中的每一個元素,但這並不是容器本身提供的能力,而是 可迭代對象 賦予了容器這種能力,當然並不是所有的容器都是可迭代的。

 

三 可迭代對象(iterable)

如果給定一個list或tuple,我們可以通過for循環來遍歷這個list或tuple,這種遍歷我們稱為迭代(Iteration)。

剛才說過,很多容器都是可迭代對象,此外還有更多的對象同樣也是可迭代對象,比如處於打開狀態的files,sockets等等。但凡是可以返回一個 迭代器 的對象都可稱之為可迭代對象,聽起來可能有點困惑,沒關系,可迭代對象與迭代器有一個非常重要的區別。先看一個例子:

這里 x 是一個可迭代對象,可迭代對象和容器一樣是一種通俗的叫法,並不是指某種具體的數據類型,list是可迭代對象,dict是可迭代對象,set也是可迭代對象。 y 和 z 是兩個獨立的迭代器,迭代器內部持有一個狀態,該狀態用於記錄當前迭代所在的位置,以方便下次迭代的時候獲取正確的元素。迭代器有一種具體的迭代器類型,比如 list_iterator , set_iterator 。可迭代對象實現了 __iter__ 和 __next__ 方法(python2中是 next 方法,python3是 __next__ 方法),這兩個方法對應內置函數 iter() 和 next() 。 __iter__ 方法返回可迭代對象本身,這使得他既是一個可迭代對象同時也是一個迭代器。

四 迭代器(iterator)

那么什么迭代器呢?它是一個帶狀態的對象,他能在你調用 next() 方法的時候返回容器中的下一個值,任何實現了 __next__() (python2中實現 next() )方法的對象都是迭代器,至於它是如何實現的這並不重要。

現在我們就以斐波那契數列()為例,學習為何創建以及如何創建一個迭代器:

著名的斐波拉契數列(Fibonacci),除第一個和第二個數外,任意一個數都可由前兩個數相加得到:

1, 1, 2, 3, 5, 8, 13, 21, 34, ...

def fab(max): 
    n, a, b = 0, 0, 1 
    while n < max: 
        print b 
        a, b = b, a + b 
        n = n + 1
code1

直接在函數fab(max)中用print打印會導致函數的可復用性變差,因為fab返回None。其他函數無法獲得fab函數返回的數列。

def fab(max): 
    L = []
    n, a, b = 0, 0, 1 
    while n < max: 
        L.append(b) 
        a, b = b, a + b 
        n = n + 1
    return L
Code2

代碼2滿足了可復用性的需求,但是占用了內存空間,最好不要。

對比for i in range(1000): pass和for i in xrange(1000): pass,前一個返回1000個元素的列表,而后一個在每次迭代中返回一個元素,因此可以使用迭代器來解決復用可占空間的問題

class Fab(object): 
    def __init__(self, max): 
        self.max = max 
        self.n, self.a, self.b = 0, 0, 1 

    def __iter__(self): 
        return self 

    def next(self): 
        if self.n < self.max: 
            r = self.b 
            self.a, self.b = self.b, self.a + self.b 
            self.n = self.n + 1 
            return r 
        raise StopIteration()


'''
>>> for key in Fabs(5):
    print key
 
     
1
1
2
3
5
'''
Code3

Fabs 類通過 next() 不斷返回數列的下一個數,內存占用始終為常數

Fib既是一個可迭代對象(因為它實現了 __iter__ 方法),又是一個迭代器(因為實現了 __next__ 方法)。實例變量 self .a 和 self.b 用戶維護迭代器內部的狀態。每次調用 next() 方法的時候做兩件事:

  1. 為下一次調用 next() 方法修改狀態
  2. 為當前這次調用生成返回結果

迭代器就像一個懶加載的工廠,等到有人需要的時候才給它生成值返回,沒調用的時候就處於休眠狀態等待下一次調用。

五 for i in (iterable)的內部實現

在大多數情況下,我們不會一次次調用next方法去取值,而是通過 for i in (iterable),

       

注意:in后面的對象如果是一個迭代器,內部因為有iter方法才可以進行操作,所以,迭代器協議里面有iter和next兩個方法,否則for語句無法應用。

 

注意:

 

for i in range(10):
        print i     :定時垃圾回收機制:沒有引用指向這個對象,則被回收

 

六 生成器(generator)

      生成器算得上是Python語言中最吸引人的特性之一,生成器其實是一種特殊的迭代器,不過這種迭代器更加優雅。代碼3遠沒有代碼1簡潔,生成器(yield)既可以保持代碼1的簡潔性,又可以保持代碼3的效果。它不需要再像上面的類一樣寫 __iter__() 和 __next__() 方法了,只需要一個 yiled 關鍵字。 生成器有如下特征是它一定也是迭代器(反之不成立),因此任何生成器也是以一種懶加載的模式生成值。用生成器來實現斐波那契數列的例子是:

def fab(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
>>> for n in fab(5):
    print n   
1
1
2
3
5

     fib 就是一個普通的python函數,它特需的地方在於函數體中沒有 return 關鍵字,函數的返回值是一個生成器對象。當執行 f=fib(5) 返回的是一個生成器對象,此時函數體中的代碼並不會執行,只有顯示或隱示地調用next的時候才會真正執行里面的代碼。

     yield 的作用就是把一個函數變成一個 generator,帶有 yield 的函數不再是一個普通函數,Python 解釋器會將其視為一個 generator,在 for 循環執行時,每次循環都會執行 fab 函數內部的代碼,執行到 yield b 時,fab 函數就返回一個迭代值,下次迭代時,代碼從 yield b 的下一條語句繼續執行,而函數的本地變量看起來和上次中斷執行前是完全一樣的,於是函數繼續執行,直到再次遇到 yield。看起來就好像一個函數在正常執行的過程中被 yield 中斷了數次,每次中斷都會通過 yield 返回當前的迭代值。

也可以手動調用 fab(5) 的 next() 方法(因為 fab(5) 是一個 generator 對象,該對象具有 next() 方法),這樣我們就可以更清楚地看到 fab 的執行流程:

>>> f = fab(3)
>>> f.__next__()
1
>>> f.__next__()
1
>>> f.__next__()
2
>>> f.__next__()
 
Traceback (most recent call last):
  File "<pyshell#62>", line 1, in <module>
    f.next()
StopIteration

 

需要明確的就是生成器也是iterator迭代器,因為它遵循了迭代器協議.

兩種創建方式

包含yield的函數

生成器函數跟普通函數只有一點不一樣,就是把 return 換成yield,其中yield是一個語法糖,內部實現了迭代器協議,同時保持狀態可以掛起。如下:

def gen():
 print 'begin: generator'
 i = 0
 while True:
  print 'before return ', i
  yield i
  i += 1
  print 'after return ', i
 
a = gen()

調用gen()並沒有真實執行函數,而是只是返回了一個生成器對象
執行第一次a.next()時,才真正執行函數,執行到yield一個返回值,然后就會掛起,保持當前的名字空間等狀態。然后等待下一次的調用,從yield的下一行繼續執行。
還有一種情況也會執行生成器函數,就是當檢索生成器的元素時,如list(generator), 說白了就是當需要數據的時候,才會執行。

生成器表達式

a = ( i for i in range(4))

return:

在一個生成器中,如果沒有return,則默認執行到函數完畢;如果遇到return,如果在執行過程中 return,則直接拋出 StopIteration 終止迭代.
def f(): yield 5
    print("ooo") return
    yield 6
    print("ppp") # if str(tem)=='None':
        # print("ok")
 f=f() # print(f.__next__()) # print(f.__next__())
for i in f: print(i) ''' return即迭代結束 for不報錯的原因是內部處理了迭代結束的這種情況 '''
return

注意:

文件讀取

def read_file(fpath): 
    BLOCK_SIZE = 1024 
    with open(fpath, 'rb') as f: 
        while True: 
            block = f.read(BLOCK_SIZE) 
            if block: 
                yield block 
            else: 
                return

如果直接對文件對象調用 read() 方法,會導致不可預測的內存占用。好的方法是利用固定長度的緩沖區來不斷讀取文件內容。通過 yield,我們不再需要編寫讀文件的迭代類,就可以輕松實現文件讀取。

 

My:生成器對象就是一種特殊的迭代器對象,滿足迭代器協議,可以調用next;對生成器對象for 循環時,調用iter方法返回了生成器對象,然后再不斷next迭代,而iter和next都是在yield內部實現的。

練習1:使用文件讀取,找出文件中最長的行的?

max(len(x.strip()) for x in open('/hello/abc','r'))
精簡答案

練習2:

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)
'''
核心語句就是:
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].
'''
解析

練習3:自定義range

七 生成器的擴展

生成器對象支持幾個方法,如gen.next() ,gen.send() ,gen.throw()等。

 由於沒有額外的yield,所以將直接拋出StopIteration。

send的工作方式:

def f():
    print("ok")
    s=yield 7
    print(s)
    yield 8

f=f()
print(f.send(None))
print(next(f))

#print(f.send(None))等同於print(next(f)),執行流程:打印ok,yield7,當再next進來時:將None賦值給s,然后返回8,可以通過斷點來觀察   

協程應用:

      所謂協同程序也就是是可以掛起,恢復,有多個進入點。其實說白了,也就是說多個函數可以同時進行,可以相互之間發送消息等。

import queue
def tt():
    for x in range(4):
        print ('tt'+str(x) )
        yield

def gg():
    for x in range(4):
        print ('xx'+str(x) )
        yield

class Task():
    def __init__(self):
        self._queue = queue.Queue()

    def add(self,gen):
        self._queue.put(gen)

    def run(self):
        while not self._queue.empty():
            for i in range(self._queue.qsize()):
                try:
                    gen= self._queue.get()
                    gen.send(None)
                except StopIteration:
                    pass
                else:
                    self._queue.put(gen)

t=Task()
t.add(tt())
t.add(gg())
t.run()

# tt0
# xx0
# tt1
# xx1
# tt2
# xx2
# tt3
# xx3
View Code

 參考:

     http://anandology.com/python-practice-book/iterators.html 

     http://www.cnblogs.com/kaituorensheng/p/3826911.html

     http://www.jb51.net/article/80740.htm

     http://www.open-open.com/lib/view/open1463668934647.html

     


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM