Date: 2019-05-23
Author: Sun
為何要引入迭代器?
? 通過列表生成式,我們可以直接創建一個列表,但是,受到內存限制,列表容量肯定是有限的,而且創建一個包含100萬個元素的列表,不僅占用很大的存儲空間,如果我們僅僅需要訪問前面幾個元素,那后面絕大多數元素占用的空間都白白浪費了。
? 我們是否可以在循環的過程中不斷推算出后續的元素呢?這樣就不必創建完整的list,從而節省大量的空間,在Python中,這種一邊循環一邊計算的機制,稱為生成器:generator
1.1 什么迭代器呢?
? 在Python中如果一個對象有__iter__( )方法和__next__( )方法,則稱這個對象是迭代器(Iterator);其中__iter__( )方法是讓對象可以用for ... in循環遍歷,next( )方法是讓對象可以通過next(實例名)訪問下一個元素。
? 注意:這兩個方法必須同時具備,才能稱之為迭代器。
? 迭代器是在python2.2中被加入的,它為類序列對象提供了一個類序列的接口。有了迭代器可以迭代一個不是序列的對象,因為他表現出了序列的行為。
? 迭代器的實質是實現了next()方法的對象,常見的元組、列表、字典都是迭代器。
迭代器中重點關注兩種方法:
__iter__方法:返回迭代器自身。可以通過python內建函數iter()調用。
__next__方法:當next方法被調用的時候,迭代器會返回它的下一個值,如果next方法被調用,但迭代器沒有只可以返回,就會引發一個StopIteration異常。該方法可以通過 python 內建函數next()調用。
舉例
內建函數iter()可以從可迭代對象中獲得迭代器。
>>> it = iter([1,2,3])
>>> next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):File "<stdin>", line 1, in <module>
StopIteration
>>>
? 迭代器是一個帶狀態的對象,他能在你調用next()
方法的時候返回容器中的下一個值,任何實現了__iter__
和__next__()
(python2中實現next()
)方法的對象都是迭代器。
? __iter__
返回迭代器自身,__next__
返回容器中的下一個值,如果容器中沒有更多元素了,則拋出StopIteration異常,至于它們到底是如何實現的這并不重要。
? 迭代器就是實現了工廠模式的對象,它在你每次你詢問要下一個值的時候給你返回。
?
1.2 如何創建一個迭代器
要創建一個迭代器有2種方法,其中前兩種分別是:
- 為容器對象添加
__iter__()
和__next__()
方法(Python 2.7 中是next()
);__iter__()
返回迭代器對象本身self
,__next__()
則返回每次調用next()
或迭代時的元素; - 內置函數
iter()
將可迭代對象轉化為迭代器
案例:創建迭代器容器
# Create iterator Object
class Container:def __init__(self, start=0, end=0):self.start = startself.end = enddef __iter__(self):print("I made this iterator!")return selfdef __next__(self):print("Calling __next__ method!")if self.start < self.end:i = self.startself.start += 1return ielse:raise StopIteration()c = Container(0, 5)
for i in c:print(i)
? 對于迭代器對象,使用for循環遍歷整個數組其實是個語法糖,他的內部實現還是通過調用對象的__next__()
方法。
? 創建迭代器對象的好處是當序列長度很大時,可以減少內存消耗,因為每次只需要記錄一個值即刻(經常看到人們介紹 Python 2.7 的 range
函數時,建議當長度太大時用 xrange
更快,在 Python 3.5 中已經去除了 xrange
只有一個類似迭代器一樣的 range
)。
1.3 斐波那契數列應用舉例
我們如果采用正常的斐波那契數列求值過程如下:
def fibs(n):if n == 0 or n == 1:return 1else:return fibs(n-1) + fibs(n-2)print([fibs(i) for i in range(10)])
自定義一個迭代器, 實現斐波那契數列
class Fib2(object):def __init__(self, n):self.a = 0self.b = 1self.n = nself.count = 0def __iter__(self):return selfdef next(self):res = self.aself.a, self.b = self.b, self.a + self.bif self.count > self.n:raise StopIterationself.count += 1return resprint(list(Fib2(10)))
其他迭代器案例
(1) 有很多關于迭代器的例子,比如itertools
函數返回的都是迭代器對象。
def test_endless_iter():'''生成無限序列:return:'''from itertools import countcounter = count(start=3)print(next(counter))print(next(counter))print(next(counter))
(2) 從一個有限序列中生成無限序列
def test_cycle_iter():'''從一個有限序列中生成無限序列:return:'''from itertools import cyclecolors = cycle(['red', 'white', 'blue'])print(next(colors))print(next(colors))print(next(colors))print(next(colors))
總結:
? 對于list、string、tuple、dict等這些容器對象,使用for循環遍歷是很方便的。在后臺for語句對容器對象調用iter()函數。iter()是python內置函數。
iter()函數會返回一個定義了next()方法的迭代器對象,它在容器中逐個訪問容器內的元素。next()也是python內置函數。在沒有后續元素時,next()會拋出一個StopIteration異常,通知for語句循環結束。
? 迭代器是用來幫助我們記錄每次迭代訪問到的位置,當我們對迭代器使用next()函數的時候,迭代器會向我們返回它所記錄位置的下一個位置的數據。實際上,在使用next()函數的時候,調用的就是迭代器對象的next方法(Python3中是對象的next方法,Python2中是對象的next()方法)。所以,我們要想構造一個迭代器,就要實現它的next方法。但這還不夠,python要求迭代器本身也是可迭代的,所以我們還要為迭代器實現iter方法,而iter方法要返回一個迭代器,迭代器自身正是一個迭代器,所以迭代器的iter方法返回自身self即可。
2. 生成器
? 生成器是一個特殊的程序,可以被用作控制循環的迭代行為,python中生成器是迭代器的一種,使用yield返回值函數,每次調用yield會暫停,而可以使用next()函數和send()函數恢復生成器。
?
生成器作用?
? 延遲操作。也就是在需要的時候才產生結果,不是立即產生結果。
什么是生成器?
? 生成器類似于返回值為數組的一個函數,這個函數可以接受參數,可以被調用,但是,不同于一般的函數會一次性返回包括了所有數值的數組,生成器一次只能產生一個值,這樣消耗的內存數量將大大減小,而且允許調用函數可以很快的處理前幾個返回值,因此生成器看起來像是一個函數,但是表現得卻像是迭代器。
? 生成器是包含yield關鍵字的函數。本質上來說,關鍵字yield是一個語法糖,內部實現支持了迭代器協議,同時yield內部是一個狀態機,維護著掛起和繼續的狀態。
python中的生成器
? 要創建一個generator,有很多種方法,第一種方法很簡單,只有把一個列表生成式的[]中括號改為()小括號,就創建一個generator
? 舉例如下:
# 列表生成式
lis = [x * x for x in range(10)]
print(lis)
# 生成器
generator_ex = (x * x for x in range(10))
print(generator_ex)結果:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
< generator
object < genexpr > at
0x000002A4CBF9EBA0 >#生成器
generator_ex = (x*x for x in range(10))
print(next(generator_ex))
print(next(generator_ex))
print(next(generator_ex))
print(next(generator_ex))#生成器, 可以直接循環得出結果
generator_ex = (x*x for x in range(10))
for i in generator_ex:print(i)結果:
0
1
4
9
生成器調用順序
那么,生成器是怎么調用執行的呢?只需要了解下面幾條規則即可:
a. 當生成器被調用的時候,函數體的代碼不會被執行,而是會返回一個迭代器,其實,生成器函數返回生成器的迭代器。 “生成器的迭代器”這個術語通常被稱作”生成器”。要注意的是生成器就是一類特殊的迭代器。作為一個迭代器,生成器必須要定義一些方法,其中一個就是next()。如同迭代器一樣,我們可以使用next()函數來獲取下一個值。需要明白的是,這一切都是在yield內部實現的。
b. 當next()方法第一次被調用的時候,生成器函數才開始執行,執行到yield語句處停止
next()方法的返回值就是yield語句處的參數(yielded value)
當繼續調用next()方法的時候,函數將接著上一次停止的yield語句處繼續執行,并到下一個yield處停止;如果后面沒有yield就拋出StopIteration異常。
c.每調用一次生成器的next()方法,就會執行生成器中的代碼,知道遇到一個yield或者return語句。yield語句意味著應該生成一個值(在上面已經解釋清楚)。return意味著生成器要停止執行,不在產生任何東西。
d. 生成器的編寫方法和函數定義類似,只是在return的地方改為yield。生成器中可以有多個yield。當生成器遇到一個yield時,會暫停運行生成器,返回yield后面的值。當再次調用生成器的時候,會從剛才暫停的地方繼續運行,直到下一個yield。生成器自身又構成一個循環器,每次循環使用一個yield返回的值。
注意:
(1)生成器是只能遍歷一次的。
(2)生成器是一類特殊的迭代器。
分類:
第一類:生成器函數:
? 生成器函數:也是用def定義的,利用關鍵字yield一次性返回一個結果,阻塞,重新開始
? 還是使用 def 定義函數,但是,使用yield而不是return語句返回結果。yield語句一次返回一個結果,在每個結果中間,掛起函數的狀態,以便下次從它離開的地方繼續執行。
如下案例加以說明:
def Fib3(max):n, a, b = 0, 0, 1while n < max:yield ba, b = b, a + bn = n + 1return '親!沒有數據了...'print("@@@@@@@@@@@@@222")
# 調用方法,生成出10個數來
f=Fib(10)
# 使用一個循環捕獲最后return 返回的值,保存在異常StopIteration的value中
while True:try:x=next(f)print("f:",x)except StopIteration as e:print("生成器最后的返回值是:",e.value)break
第二類:生成器表達式:
? 類似于列表推導,只不過是把一對大括號[]變換為一對小括號()。但是,生成器表達式是按需產生一個生成器結果對象,要想拿到每一個元素,就需要循環遍歷。
如下案例加以說明:
# 一個列表
xiaoke=[2,3,4,5]
# 生成器generator,類似于list,但是是把[]改為()
gen=(a for a in xiaoke)
for i in gen:print(i)
#結果是:
2
3
4
5
? 為什么要使用生成器?因為效率。使用生成器表達式取代列表推導式可以同時節省 cpu 和 內存(RAM)。如果你構造一個列表(list)的目的僅僅是傳遞給別的函數, 比如 傳遞給tuple()或者set(), 那就用生成器表達式替代吧!
iter
如果一個類想被用于for ... in
循環,類似list或tuple那樣,就必須實現一個__iter__()
方法,該方法返回一個迭代對象,然后,Python的for循環就會不斷調用該迭代對象的next()
方法拿到循環的下一個值,直到遇到StopIteration錯誤時退出循環。
我們以斐波那契數列為例,寫一個Fib類,可以作用于for循環:
class Fib(object):def __init__(self):self.a, self.b = 0, 1 # 初始化兩個計數器a,bdef __iter__(self):return self # 實例本身就是迭代對象,故返回自己def next(self):self.a, self.b = self.b, self.a + self.b # 計算下一個值if self.a > 100000: # 退出循環的條件raise StopIteration();return self.a # 返回下一個值
現在,試試把Fib實例作用于for循環:
>>> for n in Fib():
... print n
...
1
1
2
3
5
...
46368
75025
getitem
要表現得像list那樣按照下標取出元素,需要實現__getitem__()
方法:
class Fib(object):def __getitem__(self, n):a, b = 1, 1for x in range(n):a, b = b, a + breturn a
現在,就可以按下標訪問數列的任意一項了:
>>> f = Fib()
>>> f[0]
1
>>> f[1]
1
>>> f[2]
2
>>> f[3]
3
>>> f[10]
89
>>> f[100]
573147844013817084101
但是list有個神奇的切片方法:
>>> range(100)[5:10]
[5, 6, 7, 8, 9]
對于Fib卻報錯。原因是__getitem__()
傳入的參數可能是一個int,也可能是一個切片對象slice
,所以要做判斷:
class Fib(object):def __getitem__(self, n):if isinstance(n, int):a, b = 1, 1for x in range(n):a, b = b, a + breturn aif isinstance(n, slice):start = n.startstop = n.stopa, b = 1, 1L = []for x in range(stop):if x >= start:L.append(a)a, b = b, a + breturn L
現在試試Fib的切片:
>>> f = Fib()
>>> f[0:5]
[1, 1, 2, 3, 5]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
但是沒有對step參數作處理:
>>> f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
也沒有對負數作處理,所以,要正確實現一個__getitem__()
還是有很多工作要做的。
文件是可迭代對象,也是迭代器
f = open('housing.csv')
from collections import Iterator
from collections import Iterableprint(isinstance(f, Iterator)) #判斷是不是迭代器
print(isinstance(f, Iterable)) #判斷是不是可迭代對象True
True
小結:
- 凡是可作用于
for
循環的對象都是Iterable
類型; - 凡是可作用于
next()
函數的對象都是Iterator
類型,它們表示一個惰性計算的序列; - 集合數據類型如
list
、dict
、str
等是Iterable
但不是Iterator
,不過可以通過iter()
函數獲得一個Iterator
對象。
Python3的for
循環本質上就是通過不斷調用next()
函數實現的,例如:
for x in [1, 2, 3, 4, 5]:pass
實際上完全等價于
# 首先獲得Iterator對象:
it = iter([1, 2, 3, 4, 5])
# 循環:
while True:try:# 獲得下一個值:x = next(it)except StopIteration:# 遇到StopIteration就退出循環break
yield的總結
(1)通常的for..in...循環中,in后面是一個數組,這個數組就是一個可迭代對象,類似的還有鏈表,字符串,文件。他可以是a = [1,2,3],也可以是a = [x*x for x in range(3)]。
它的缺點也很明顯,就是所有數據都在內存里面,如果有海量的數據,將會非常耗內存。
(2)生成器是可以迭代的,但是只可以讀取它一次。因為用的時候才生成,比如a = (x*x for x in range(3))。!!!!注意這里是小括號而不是方括號。
(3)生成器(generator)能夠迭代的關鍵是他有next()方法,工作原理就是通過重復調用next()方法,直到捕獲一個異常。
(4)帶有yield的函數不再是一個普通的函數,而是一個生成器generator,可用于迭代
(5)yield是一個類似return 的關鍵字,迭代一次遇到yield的時候就返回yield后面或者右面的值。而且下一次迭代的時候,從上一次迭代遇到的yield后面的代碼開始執行
(6)yield就是return返回的一個值,并且記住這個返回的位置。下一次迭代就從這個位置開始。
(7)帶有yield的函數不僅僅是只用于for循環,而且可用于某個函數的參數,只要這個函數的參數也允許迭代參數。
(8)send()和next()的區別就在于send可傳遞參數給yield表達式,這時候傳遞的參數就會作為yield表達式的值,而yield的參數是返回給調用者的值,也就是說send可以強行修改上一個yield表達式值。
(9)send()和next()都有返回值,他們的返回值是當前迭代遇到的yield的時候,yield后面表達式的值,其實就是當前迭代yield后面的參數。
(10)第一次調用時候必須先next()或send(),否則會報錯,send后之所以為None是因為這時候沒有上一個yield,所以也可以認為next()等同于send(None)