本文部分參考: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有兩種不同的方式提供生成器:
- 生成器函數:常規函數定義,但是,使用yield語句而不是return語句返回結果。 yield語句一次返回一個結果,在每個結果中間,掛起函數的狀態,以便下次重它離開的地方繼續執行
- 生成器表達式:類似於列表推導,但是,生成器返回按需產生結果的一個對象, 而不是一次構建一個結果列表
我們先用一個例子說明一下:
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的關係:
補充幾個小例子:
a)使用生成器創建一個range
def range(n):
count = 0
while count < n:
yield count
count += 1