python講稿5 迭代器,可迭代對象,生成器

本文部分參考:Python迭代器,生成器–精華中的精華 https://www.cnblogs.com/deeper/p/7565571.html

一 迭代器和可迭代對象

迭代器是訪問集合元素的一種方式。。迭代器只能往前不會後退。迭代器的一大優點是不要求事先準備好整個迭代過程中所有的元素,僅僅在迭代到某個元素時才計算該元素,而在這之前或之後,元素可以不存在或者被銷燬。這個特點使得它特別適合用於遍歷一些巨大的或是無限的集合,比如幾個G的文件。

特點:

a)訪問者不需要關心迭代器內部的結構,僅需通過next()方法或不斷去取下一個內容

b)不能隨機訪問集合中的某個值 ,只能從頭到尾依次訪問

c)訪問到一半時不能往回退

d)便於循環比較大的數據集合,節省內存

e)也不能複製一個迭代器。如果要再次(或者同時)迭代同一個對象,只能去創建另一個迭代器對象。

enumerate()的返回值就是一個迭代器,我們以enumerate爲例:

a = enumerate(['a','b'])

for i in range(2):    #迭代兩次enumerate對象
    for x, y in a:
        print(x,y)
    print(''.center(50,'-'))

結果:

0 a
1 b
-----------------------分割線------------------------
-----------------------分割線------------------------

可以看到再次迭代enumerate對象時,沒有返回值。

我們可以用linux的文件處理命令vim和cat來理解一下:

a) 讀取很大的文件時,vim需要很久,cat是毫秒級;因爲vim是一次性把文件全部加載到內存中讀取;而cat是加載一行顯示一行

b) vim讀寫文件時可以前進,後退,可以跳轉到任意一行;而cat只能向下翻頁,不能倒退,不能直接跳轉到文件的某一頁(因爲讀取的時候這個“某一頁“可能還沒有加載到內存中)。

正式進入python迭代器之前,我們先要區分兩個容易混淆的概念:可迭代對象(Iterable)和迭代器(Iterator)

1.1可迭代對象

定義:迭代器是一個對象,不是一個函數。只要它定義了可以返回一個迭代器的__iter__方法,或者定義了可以支持下標索引的__getitem__方法,那麼它就是一個可迭代對象。

注意: 集合數據類型,如list、tuple、dict、set、str都是可迭代對象Iterable,卻不是迭代器Iterator。

如何判斷一個對象是可迭代對象呢?可以通過collections模塊的Iterable類型判斷:

from collections import Iterable
print(isinstance([], Iterable))
print(isinstance({}, Iterable))
print(isinstance('abc', Iterable))
print(isinstance({'name':'join','age':23},Iterable))
print(isinstance(set([2,3]),Iterable))

結果爲:

True
True
True
True
True

再看:

from collections import Iterator
print(isinstance([], Iterator))
print(isinstance({}, Iterator))
print(isinstance('abc', Iterator))
print(isinstance({'name':'join','age':23},Iterator))
print(isinstance(set([2,3]),Iterator))

結果爲:

False
False
False
False
False

1.2迭代器

定義:任何實現了__iter__()和__next__()(python2中實現next())方法的對象都是迭代器,__iter__返回迭代器自身,__next__返回容器中的下一個值。

這裏我們來看一下迭代器和可迭代對象的使用方法。

問題:不適用for,while以及下表索引,怎麼遍歷一個list?

l1=[2,3,5]
iter_l1 = l1.__iter__() 
# 或者iter(iter_l1)
# iter(iter_l1)是python的內置函數,
# l1.__iter__()調用的是l1對象的__iter__()方法。
# 下面的next()函數和__iter__()函數類似。
print(next(iter_l1)) # 或print(iter_l1.__next__()
print(next(iter_l1))
print(next(iter_l1))
print(next(iter_l1))

結果爲:

2
3
5
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-126-eb58865cb644> in <module>
      4 print(next(iter_l1))
      5 print(next(iter_l1))
----> 6 print(next(iter_l1))

結論:思路就是,先得到當前對象(當前對象是可迭代對象)的迭代器,然後每次執行next(iter_l1)就可以得到當前對象的一個值。

修改一下:

l1=[2,3,5]
iter_l1 = l1.__iter__() 
for i in iter_l1:
    print(i,end=',')

結果爲:

2,3,5,

再次執行for語句如下:

for i in iter_l1:
    print(i,end=',')

此次返回結果爲空,可以看到迭代器只能遍歷一次對象,不能重複遍歷。由於for語句可以自動處理StopIteration異常,所以這裏沒有報出StopIteration,而是沒有任何結果。

看一個例子。我們想得到Fibonacci數列,思路是定義一個可迭代對象類Fib;然後在定義一個迭代器類FibIterator,使用方式是:
① 實例化一個可迭代對象: fib = Fib()
② 得到fib的一個迭代器: fib_iter = iter(fib)
③ 然後每次調用next(fib_iter)可得到fibonacci數列中的一個數。

class FibIterator():
    '''
    定義迭代器類
    '''
    def __init__(self,num,a,b,current):
        self.num = num
        self.a = a
        self.b = b
        self.current = current
    def __iter__(self):
        return self
    def __next__(self):
        if(self.num-1>=0):
            self.num = self.num-1
            self.current = self.a
            self.a = self.b
            self.b = self.b+self.current   #以上兩步賦值操作可省略中間變量直接寫爲self.a,self.b = self.b,self.a+self,b 
            return self.current
        else: raise StopIteration
         
class Fib:
    '''
    定義可迭代對象所屬類
    '''
    def __init__(self,num): #num表示該數列的長度
        self.a = 1
        self.b = 2
        self.current=self.a
        self.num = num
    def __iter__(self):
        return FibIterator(self.num,self.a,self.b,self.current)
        
fib = Fib(20)
fib_iter = iter(fib)
for i in range(20):
    print(next(fib_iter),end=',')

結果爲:

1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,

這裏定義的Fibonacci類的__init__函數有一個參數num,表示一共得到幾個fibonacci數。

由於可迭代對象是實現了__iter__()方法的,迭代器對象是實現了__iter__()和__next__()方法的,能否只定義一個迭代器類呢?

試一下。

class Fib:
    def __init__(self,num):
        self.num = num
        self.a = 1
        self.b = 2
        self.current = self.a
    def __iter__(self):
        return self
    def __next__(self):
        if(self.num-1>=0):
            self.num = self.num-1
            self.current = self.a
            self.a = self.b
            self.b = self.b+self.current   #以上兩步賦值操作可省略中間變量直接寫爲self.a,self.b = self.b,self.a+self,b 
            return self.current
        else: raise StopIteration
        
fib = Fib(10)
for i in fib:
    print(i,end=',')

結果爲:

1,2,3,5,8,13,21,34,55,89,

當我們再次執行for語句時,

for i in fib:
    print(i,end=',')

同上面的分析一樣,結果爲空。如果想再次得到fibonacci數列的前10個數,就必須重新實例化Fib對象。

結論:爲什麼不只保留Iterator的接口而還需要設計Iterable呢?

因爲迭代器迭代一次以後就空了,那麼如果list,dict也是一個迭代器,迭代一次就不能再繼續被迭代了,這顯然是反人類的;所以通過__iter__每次返回一個獨立的迭代器,就可以保證不同的迭代過程不會互相影響。

另外,迭代器是惰性的,只有在需要返回下一個數據時它纔會計算。所以,Iterator甚至可以表示一個無限大的數據流,例如全體自然數。而使用list是永遠不可能存儲全體自然數的。

下面的例子得到全體自然數(下面我們用生成器可以得到更簡單的寫法)。

class Natural:
    def __init__(self):
        pass
    def __iter__(self):
        return NaturalIterator()
class NaturalIterator:
    def __init__(self):
        self.beg=0
        self.current=self.beg
    def __iter__(self):
        return self
    def __next__(self):
        self.current += 1
        return self.current   

# 顯示前20個自然數
n1=Natural()
n1_iter = iter(n1)
for i in range(20):
    print(next(n1_iter),end=',')

結果爲:

1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,

作業:使用迭代器和可迭代對象實現得到全體fibonacci數。

二 生成器

2.1 生成器的定義和使用

定義:生成器其實是一種特殊的迭代器,它不需要再像上面的類一樣寫__iter__()和__next__()方法了,只需要一個yiled關鍵字。

生成器一定是迭代器(反之不成立)。

Python有兩種不同的方式提供生成器:

  1. 生成器函數:常規函數定義,但是,使用yield語句而不是return語句返回結果。 yield語句一次返回一個結果,在每個結果中間,掛起函數的狀態,以便下次重它離開的地方繼續執行
  2. 生成器表達式:類似於列表推導,但是,生成器返回按需產生結果的一個對象, 而不是一次構建一個結果列表

我們先用一個例子說明一下:

def generate_even():
    i=0
    while True:
        if i%2==0:
            yield i
        i += 1

g=generate_even()
dir(g) # 可以看到裏面有__iter__()方法和__next__()方法,所以生成器也是迭代器。

for i in range(10):
    print(g.__next__(),end=',')

結果爲:

0,2,4,6,8,10,12,14,16,18,

現在解釋一下上面的代碼:

我們知道,一個函數只能返回一次,即return以後,這次函數調用就結束了;

但是生成器函數可以暫停執行,並且通過yield返回一箇中間值,當生成器對象的__next__()方法再次被調用的時候,生成器函數可以從上一次暫停的地方繼續執行,直到下一次遇到yield語句(此時會返回yield後面的值,如果有的話)或者觸發一個StopIteration。

瞭解協同程序:
a) 生成器的另外一個方面是協同程序的概念。協同程序是可以運行的獨立函數調用,可以暫停或者掛起,並從程序離開的地方繼續或者重新開始。

b) 可以在調用者和被調用的之間協同程序通信。

c) 在程序暫停時可以傳參:舉例來說,當協同程序暫停時,我們仍可以從其中獲得一箇中間的返回值,當調用回到程序中時,能夠傳入額外或者改變了的參數,但是仍然能夠從我們上次離開的地方繼續,並且所有狀態完整。

生成器表達式類似於列表推導式,只是把[]換成(),這樣就創建了一個生成器。

gen = (x for x in range(10))

下面我們用生成器來實現前面的fibonacci數列和全體自然數。

# 生成前n個fibonacci數
def fib(n):
    a, b = 0, 1
    count=0
    while True:
        if(count>n):
            break
        count += 1
        yield b
        a, b = b, a+b

f = fib(20)
for item in f:
    print(item,end=',')

結果爲:

1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,
def Natural():
    n=0
    while True:
        yield n
        n += 1
g_n1=Natural()
for i in range(20):
    print(next(g_n1),end=',')

結果爲:

0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,

2.2 send方法

生成器函數最大的特點是可以接受外部傳入的一個變量,並根據變量內容計算結果後返回。這是生成器函數最難理解的地方,也是最重要的地方,協程的實現就全靠它了。

看一個小貓吃魚的例子:

def cat():
    print('我是一隻hello kitty')
    while True:
        food = yield
        if food == '魚肉':
            yield '好開心'
        else:
            yield '不開心,人家要吃魚肉啦'

中間有個賦值語句food = yield,可以通過send方法來傳參數給food,試一下:

情況1)

miao = cat()    #只是用於返回一個生成器對象,cat函數不會執行
print(''.center(50,'-'))
print(miao.send('魚肉'))

結果:

Traceback (most recent call last):
--------------------------------------------------
  File "C:/Users//Desktop/Python/cnblogs/subModule.py", line 67, in <module>
    print(miao.send('魚肉'))
TypeError: can't send non-None value to a just-started generator

看到了兩個信息:

a)miao = cat() ,只是用於返回一個生成器對象,cat函數不會執行

b)can’t send non-None value to a just-started generator;不能給一個剛創建的生成器對象直接send值

改一下,情況2)

miao = cat()
miao.__next__()
print(miao.send('魚肉'))

那麼到底send()做了什麼呢?send()的幫助文檔寫的很清楚,’’‘Resumes the generator and “sends” a value that becomes the result of the current yield-expression.’’’;可以看到send依次做了兩件事:

a)回到生成器掛起的位置,繼續執行

b)並將send(arg)中的參數賦值給對應的變量,如果沒有變量接收值,那麼就只是回到生成器掛起的位置

但是,我認爲send還做了第三件事:

c)兼顧__next__()作用,掛起程序並返回值,所以我們在print(miao.send(‘魚肉’))時,纔會看到’好開心’;其實__next__()等價於send(None)

所以當我們嘗試這樣做的時候:

def cat():
    print('我是一隻hello kitty')
    while True:
        food = yield
        if food == '魚肉':
            yield '好開心'
        else:
            yield '不開心,人家要吃魚肉啦'

miao = cat()
print(miao.__next__())
print(miao.send('魚肉'))
print(miao.send('骨頭'))
print(miao.send('雞肉'))

就會得到這個結果:

我是一隻hello kitty
None
好開心
None
不開心,人家要吃魚肉啦

我們按步驟分析一下:

a)執行到print(miao.next()),執行cat()函數,print了”我是一隻hello kitty”,然後在food = yield掛起,並返回了None,打印None

b)接着執行print(miao.send(‘魚肉’)),回到food = yield,並將’魚肉’賦值給food,生成器函數恢復執行;直到運行到yield ‘好開心’,程序掛起,返回’好開心’,並print’好開心’

c)接着執行print(miao.send(‘骨頭’)),回到yield ‘好開心’,這時沒有變量接收參數’骨頭’,生成器函數恢復執行;直到food = yield,程序掛起,返回None,並print None

d)接着執行print(miao.send(‘雞肉’)),回到food = yield,並將’雞肉’賦值給food,生成器函數恢復執行;直到運行到yield’不開心,人家要吃魚肉啦’,程序掛起,返回’不開心,人家要吃魚肉啦’,並print ‘不開心,人家要吃魚肉啦’

大功告成,那我們優化一下代碼:

def cat():
    msg = '我是一隻hello kitty'
    while True:
        food = yield msg
        if food == '魚肉':
            msg = '好開心'
        else:
            msg = '不開心,人家要吃魚啦'

miao = cat()
print(miao.__next__())
print(miao.send('魚肉'))
print(miao.send('雞肉'))

我們再看一個更實用的例子,一個計數器。

def counter(start_at = 0):
    count = start_at
    while True:
        val = yield count
        if val is not None:
            count = val
        else:
            count += 1

count = counter(5)
print(count.__next__())
print(count.send(0))

結果爲5,0而不是5,6的原因:

①執行print(count.next()),程序運行到val = yield count(第一個yield語句)後掛起,然後返回yield後面的值,所以結果爲5。

②執行print(count.send(0)),程序恢復到掛起點val = yield count,將send的參數0賦值給接受變量val,然後繼續執行下面的語句,由於val=0,所以if val is not None條件爲真,count = val,接着又來到yield語句(val = yield count),此時程序掛起,返回yield後面的值count=0。

綜上:當執行next()方法時,程序會恢復到掛起點,依次執行yield語句下面的語句,最後再返回yield後面的值;而不是先返回yield後面的值,再執行yield後面的語句。

最後給出一張圖說明Iterable,Iterator,Generator的關係:
gen2

補充幾個小例子:

a)使用生成器創建一個range

def range(n):
    count = 0
    while count < n:
        yield count
        count += 1
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章