python迭代器和可迭代对象
1. 迭代器 vs 可迭代對象
python中兩個(gè)迭代的概念,一個(gè)叫做迭代器(Iterator),一個(gè)叫做可迭代對象(Iterable),我們可以從collections模塊中導(dǎo)入
from collections.abc import Iterable,Iterator當(dāng)我們實(shí)現(xiàn)了迭代器之后,就可以使用for循環(huán)進(jìn)行遍歷了。我們平常使用的字符串,列表,元組和字典等,底層都實(shí)現(xiàn)了迭代器。我們可以通過instance來判斷
from collections.abc import Iterable,Iterators = "abcdefgh" print(isinstance(s,Iterable)) # True print(isinstance(s,Iterator)) # Falsel = [1,2,3,4,5,6,7,8] print(isinstance(s,Iterable)) # True print(isinstance(s,Iterator)) # Falset = (1,2,3,4,5,6,7,8) print(isinstance(s,Iterable)) # True print(isinstance(s,Iterator)) # False哈哈,上來就打臉了,發(fā)現(xiàn)字符串,列表和元組并不是迭代器,而是迭代對象。不要緊,繼續(xù)往下看
2. 如何實(shí)現(xiàn)迭代器
迭代器的實(shí)現(xiàn)非常簡單,只需要實(shí)現(xiàn)__iter__和__next__這兩個(gè)魔法函數(shù)即可,
我們來實(shí)現(xiàn)一個(gè)斐波那契數(shù)列
from collections.abc import Iterable,Iteratorclass Fib:def __init__(self):self.prev = 0self.curr = 1def __iter__(self): # 自身就是迭代器,所以返回自身return selfdef __next__(self): # 只有實(shí)現(xiàn)了__next__函數(shù)才是迭代器print('run __next__ func')self.prev,self.curr = self.curr,self.curr+self.prevreturn self.currfib = Fib() print(isinstance(fib,Iterator)) # True print(isinstance(fib,Iterable)) # True # 通過next函數(shù)獲取下一個(gè)值 print(next(fib)) # 1 print(next(fib)) # 2 print(next(fib)) # 3 print(next(fib)) # 5索然無味啊,這迭代器好像并沒有什么特別的地方。每次調(diào)用next函數(shù),就會(huì)進(jìn)入到__next__函數(shù)中,然后計(jì)算curr的值,返回出來。
上面實(shí)現(xiàn)的迭代器是沒有終止條件的,只要你愿意就可以一直計(jì)算下去,但是一般的迭代器都是有終止條件的,我們修改一下上面的迭代器
from collections.abc import Iterable,Iterator from operator import indexclass Fib:def __init__(self, end):self.prev = 0self.curr = 1self.end = endself.index = 0def __iter__(self):return selfdef __next__(self):if self.index < self.end:self.prev,self.curr = self.curr,self.curr+self.prevself.index += 1return self.currelse:raise StopIterationfib = Fib(3)print(next(fib)) # 1 print(next(fib)) # 2 print(next(fib)) # 3可以看到正常輸出了,如果我們再次調(diào)用next呢?
next(fib)Traceback (most recent call last):
File “fib.py”, line 26, in
print(next(fib)) # 5
File “fib.py”, line 19, in next
raise StopIteration
StopIteration
可以看到,我們得到一個(gè)異常對象,注意,主動(dòng)拋出來的是異常對象,而不是異常,說明我們訪問結(jié)束了。for循環(huán)幫我們處理了異常
for v in fib:print(v)for循環(huán)實(shí)現(xiàn)的邏輯是這樣的
# create an iterator object from that iterable iter_obj = iter(iterable)# infinite loop while True:try:# get the next itemelement = next(iter_obj)print(element)# do something with elementexcept StopIteration:# if StopIteration is raised, break from loopbreak就很有意思,捕獲的是StopIteration,那如果我拋出的不是這個(gè)異常呢,比如我拋出一個(gè)IndexError
import timeclass Fib:def __init__(self, end):self.prev = 0self.curr = 1self.end = endself.index = 0def __iter__(self):return selfdef __next__(self):if self.index < self.end:self.prev,self.curr = self.curr,self.curr+self.prevself.index += 1time.sleep(0.1)print('.',end='',flush=True)return self.currelse:raise IndexError('index > end')fib = Fib(10) for v in fib:print(v)Traceback (most recent call last):
File “fib.py”, line 20, in
for v in fib:
File “fib.py”, line 17, in next
raise IndexError(‘index > end’)
IndexError: index > end
可以看到for循環(huán)并沒有捕獲這個(gè)異常,而是直接拋出來了。后來我一尋思,覺得理應(yīng)如此,for循環(huán)之需要判斷是否完成遍歷即可,其他的異常應(yīng)該交給用戶自己處理。
3. 迭代器的好處
回想一下我們平時(shí)是怎么實(shí)現(xiàn)斐波那契數(shù)列的
3.1 每次從頭計(jì)算(空間換時(shí)間)
這種方式非常的耗時(shí),每次都要重頭開始計(jì)算,但好處是想要獲取哪一位的數(shù)值直接調(diào)用即可,沒有任何約束和依賴
import time def fib(end):i = 0prev, curr = 0, 1print('calc {}th '.format(end),end='')while i < end:prev, curr = curr, prev + curri += 1time.sleep(0.1)print('.',end='',flush=True)return curr start = time.perf_counter() for i in range(10):print(fib(i)) end = time.perf_counter() print('fib cost time {}'.format(end-start))3.2 結(jié)果保存下來(時(shí)間換空間)
相對于從頭計(jì)算,我們可以將所有的結(jié)果保存下來,這種方式只需要計(jì)算一次,就可以獲取之前任意一次的結(jié)果,因?yàn)樗薪Y(jié)果都保存下來了,所以很占空間。
import time def fib(end):print('calc {} '.format(end),end='')res = [1]i = 0prev, curr = 0, 1while i < end:prev, curr = curr, prev + curri += 1res.append(curr)time.sleep(0.1)print('.',end='',flush=True)print('\n')return resstart = time.perf_counter() res = fib(10) for i in range(10):print(res[i]) end = time.perf_counter() print('fib cost time {}'.format(end-start))3.3 迭代器實(shí)現(xiàn)
迭代器的實(shí)現(xiàn)前面已經(jīng)說過了,很有意思的一點(diǎn)是,我們只有調(diào)用了next函數(shù),迭代器才會(huì)返回我們下一個(gè)結(jié)果。如果我想要獲取第5個(gè)斐波那契數(shù)列值,我需要調(diào)用5次next函數(shù),每調(diào)用一次就會(huì)執(zhí)行一次__next__函數(shù)。
import timeclass Fib:def __init__(self, end):self.prev = 0self.curr = 1self.end = endself.index = 0def __iter__(self):return selfdef __next__(self):print('calc',end='')if self.index < self.end:self.prev,self.curr = self.curr,self.curr+self.prevself.index += 1time.sleep(0.1)print('.',end='',flush=True)return self.currelse:raise StopIteration start = time.perf_counter() fib = Fib(10) for v in fib:print(v) end = time.perf_counter() print('fib cost time {}'.format(end-start))4 迭代器的局限
迭代器不基于索引的方式獲取可迭代對象中的元素。節(jié)省了大量的內(nèi)存,迭代器在內(nèi)存中相當(dāng)于只占一個(gè)數(shù)據(jù)的空間。因?yàn)槊看稳≈刀忌弦粭l數(shù)據(jù)會(huì)在內(nèi)存釋放,加載當(dāng)前的此條數(shù)據(jù)。惰性機(jī)制非常有用,我們平時(shí)處理大文件的時(shí)候,沒有辦法一下子讀到內(nèi)存中來,就可以通過迭代器的方式一條一條的讀取。
迭代器取值時(shí)不走回頭路,只能一直向下取值,所以不能直觀的查看里面的數(shù)據(jù)。老實(shí)講,我覺得索引的方式非常的好,想要獲取那個(gè)位置的數(shù)據(jù),直接就可以獲取,歷史所有的狀態(tài)都保留了下來,而迭代器則需要一步一步計(jì)算過去。所以說,迭代器是一個(gè)雙刃劍,就看你想要在什么場景下使用
可迭代對象
可迭代對象就非常簡單了,迭代器需要實(shí)現(xiàn)兩個(gè)函數(shù),一個(gè)是__next__用來計(jì)算下一個(gè)值,一個(gè)是__iter__返回自身,因?yàn)樽陨砭褪堑鳌?傻鷮ο笾恍枰獙?shí)現(xiàn)一個(gè)__iter__函數(shù)就可以了,這個(gè)函數(shù)返回一個(gè)迭代器。
from collections.abc import Iterable,Iterator from operator import indexclass Fib:def __init__(self, end):self.prev = 0self.curr = 1self.end = endself.index = 0def __iter__(self):return selfdef __next__(self):if self.index < self.end:self.prev,self.curr = self.curr,self.curr+self.prevself.index += 1return self.currelse:raise StopIterationclass Fibable:def __init__(self, end):self.end = enddef __iter__(self):return Fib(self.end)fib = Fibable(10) print(isinstance(fib,Iterable)) # True 是可迭代對象 print(isinstance(fib,Iterator)) # False 不是迭代器 for i in fib:print(i)但是可迭代對象沒有實(shí)現(xiàn)__next__函數(shù),因此無法通過next來獲取下一個(gè)元素,只能通過for循環(huán)獲取數(shù)據(jù),如果調(diào)用next函數(shù)則會(huì)報(bào)錯(cuò),而且可以看到,報(bào)的是類型錯(cuò)誤,next函數(shù)只能接受迭代器。
next(fib)Traceback (most recent call last):
File “fib.py”, line 38, in
next(fib)
TypeError: ‘Fibable’ object is not an iterator
自定義可迭代數(shù)據(jù)
python中還提供了一個(gè)魔法函數(shù)__getitem__,實(shí)現(xiàn)這個(gè)函數(shù)之后,就可以使用for循環(huán)遍歷了,pytorch中的dataloader就是這種方式,先舉個(gè)簡單的例子。
from collections.abc import Iterable,Iteratorclass Fib:def __init__(self) -> None:self.res = [0,1,1,2,3,5,8,13,21,35,56,91]def __getitem__(self,index):print('run fib func...')return self.res[index]fib = Fib() print(isinstance(fib,Iterable)) # False 不是可迭代對象 print(isinstance(fib,Iterator)) # False 不是迭代器 for v in fib: # 啥也不是,但就是可以for循環(huán)print(v)Fib既不是可迭代對象,也不是迭代器,但是可以使用for循環(huán)來遍歷數(shù)據(jù)。而且發(fā)現(xiàn)__getitem__還有一個(gè)入?yún)ndex,這說明我們其實(shí)可以直接通過索引遍歷數(shù)據(jù)。看看斐波那契數(shù)列如何實(shí)現(xiàn)
from collections.abc import Iterable,Iteratorclass Fib:def __init__(self) -> None:passdef __getitem__(self,index):i = 0prev, curr = 0, 1while i < index:print('calc fib...')prev, curr = curr, prev + curri += 1return currfib = Fib() print(isinstance(fib,Iterable)) # False 不是可迭代對象 print(isinstance(fib,Iterator)) # False 不是迭代器print(fib[3])上面實(shí)現(xiàn)了斐波那契數(shù)列,我們沒有保存結(jié)果,每次讀取結(jié)果,每次重頭開始計(jì)算。簡直是無話可說,這就相當(dāng)于第一種最耗時(shí)的方案,非常的愚蠢。我可以稍作修改,變成第二種方式,把所有的結(jié)果再初始化的時(shí)候都計(jì)算一遍然后保存下來,而且可以隨意索引,就是占空間。
from collections.abc import Iterable,Iteratorclass Fib:def __init__(self, end):# 初始化的時(shí)候?qū)⑺械慕Y(jié)果計(jì)算一遍,然后保存下來self.res = []i = 0prev, curr = 0, 1while i < end:prev, curr = curr, prev + curri += 1self.res.append(curr)def __getitem__(self,index):return self.res[index]fib = Fib(10) print(isinstance(fib,Iterable)) # False 不是可迭代對象 print(isinstance(fib,Iterator)) # False 不是迭代器# 可以使用for循環(huán) for v in fib:print(v)print(fib[3]) # 可以通過索引隨意取值其實(shí)我覺得這就是一個(gè)單純的get方法而已,因?yàn)槠鋵?shí)還有一個(gè)方法,叫做__setitem__,這不就是平時(shí)使用的@pro和@name.setter嗎?
python提供了iter函數(shù),可以將__getitem__轉(zhuǎn)變稱迭代器
胡亂測試
可迭代對象只實(shí)現(xiàn)了__iter__,如果我再實(shí)現(xiàn)了__next__會(huì)不會(huì)變成迭代器
from collections.abc import Iterable,Iterator from operator import indexclass Fib:def __init__(self, end):self.prev = 0self.curr = 1self.end = endself.index = 0def __iter__(self): # 本身就是迭代器,直接返回自己return selfdef __next__(self):if self.index < self.end:self.prev,self.curr = self.curr,self.curr+self.prevself.index += 1return self.currelse:raise StopIterationclass Fibable:def __init__(self, end):self.end = enddef __iter__(self): # 要求返回一個(gè)迭代器return Fib(self.end)def __next__(self):print('next func ...')fib = Fibable(10) print(isinstance(fib,Iterable)) # True 可迭代對象 print(isinstance(fib,Iterator)) # True 不是迭代器 for i in fib:print(i)萬萬沒想到,竟然真的變成迭代器了,而且可以正常打印出結(jié)果。這說明,在迭代的過程中并沒有調(diào)用__next__,而是調(diào)用了__iter__函數(shù)。這說明如果同時(shí)存在的話,會(huì)優(yōu)先調(diào)用__iter__。再嘗試使用next執(zhí)行看看
next(fib) # next func ... next(fib) # next func ... next(fib) # next func ...輸出的是__next__的結(jié)果,而不是__iter__的結(jié)果。
如果同時(shí)存在__getitem__和__next__會(huì)怎么樣呢?
from collections.abc import Iterable,Iterator from operator import indexclass Fib:def __init__(self):passdef __iter__(self):return selfdef __next__(self):print('next func')def __getitem__(self,index):print('getitem func {}'.format(index))fib = Fib() print(isinstance(fib,Iterable)) # True 是可迭代對象 print(isinstance(fib,Iterator)) # True 是迭代器 i = 0 for v in fib:if i > 10:breaki += 1next(fib) next(fib) next(fib)fib[3]for會(huì)發(fā)現(xiàn)優(yōu)先調(diào)用__next__函數(shù),使用next函數(shù)調(diào)用__next__函數(shù),使用索引調(diào)用__getitem__函數(shù)
總結(jié)
- 迭代器:實(shí)現(xiàn)__iter__和__next__兩個(gè)魔法函數(shù),可以使用for循環(huán)和next
- 可迭代對象:只能實(shí)現(xiàn)__iter__函數(shù),并且這個(gè)函數(shù)返回的是一個(gè)迭代器,可以使用for,由于沒有實(shí)現(xiàn)__next__函數(shù),所以不能使用next
- 自定義可迭代數(shù)據(jù):實(shí)現(xiàn)__getitem__函數(shù),有一個(gè)入?yún)ndex,意味著我們可以通過索引訪問,所以也就意味著需要保存較多的數(shù)據(jù)在內(nèi)存中,通過iter函數(shù)可以將自定義的迭代數(shù)據(jù)轉(zhuǎn)換成迭代器
個(gè)人的一些見解
老實(shí)講,我覺得迭代器還是比較雞肋的,為了計(jì)算下一個(gè)狀態(tài),需要保存上下文,然后通過處理得到新的狀態(tài)。如果我們想要實(shí)現(xiàn)的就是在循環(huán)中不斷往下迭代,不需要之前的狀態(tài),那么可以使用迭代器,如果想要通過索引來快速得到想要的結(jié)果,最好還是使用自定義的迭代對象。
應(yīng)用舉例
我能想到使用迭代器的場景,一般會(huì)滿足兩個(gè)條件
例如我們想要處理1T的數(shù)據(jù),不會(huì)直接加載到內(nèi)存中,而是一條一條的加載,這個(gè)時(shí)候我們就可以把所有的路徑加載進(jìn)來,然后讀取每個(gè)路徑的文件,迭代的對這些數(shù)據(jù)進(jìn)行處理。
總結(jié)
以上是生活随笔為你收集整理的python迭代器和可迭代对象的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VUE和js请求接口400错误
- 下一篇: python 接口自动化测试王浩然 pd