Python基础教程:迭代器
迭代是什么
迭代指的是一個(gè)重復(fù)的過程,每次重復(fù)都必須基于上一次的結(jié)果而繼續(xù),單純的重復(fù)并不是迭代,如Python中的for循環(huán)就是一個(gè)非常好的迭代例子。
for item in range(10):print(item)迭代必須向前推進(jìn),不能后退,如下所示:
# [0 , 1, 2, 3, 4, 5, 6, 7, 8, 9] # ------------------------------>下面這種方式就不屬于迭代:
# [0 , 1, 2, 3, 4, 5, 6, 7, 8, 9] # --------> # <---- # --------------------->迭代器協(xié)議
在學(xué)習(xí)迭代器的整個(gè)知識(shí)點(diǎn)中,迭代器協(xié)議占據(jù)了非常重要的位置。
迭代器協(xié)議中包含了2個(gè)最基本的概念,分別是可迭代對(duì)象和迭代器對(duì)象。
- 可迭代對(duì)象(Iterable):內(nèi)部實(shí)現(xiàn)了__iter__()方法的對(duì)象則被稱為可迭代對(duì)象
- 迭代器對(duì)象(Iterator):內(nèi)部實(shí)現(xiàn)了__next__()方法的對(duì)象則被稱之為迭代器對(duì)象
兩者之間的關(guān)系:
- 在Python中,迭代器對(duì)象一定屬于可迭代對(duì)象范疇,也就說迭代器對(duì)象必須具有__iter__()方法以及__next__()方法
- 在Python中,可迭代對(duì)象不一定屬于迭代器對(duì)象范疇,也就是說可迭代對(duì)象只需要實(shí)現(xiàn)__iter__()方法即可
介紹2個(gè)函數(shù):
- iter(Object)函數(shù),它底層會(huì)執(zhí)行Object.__iter__()方法
- next(Object)函數(shù),它底層會(huì)執(zhí)行Object.__next__()方法
內(nèi)置類型
通過collections.abc下的Iterable類和Iterator類進(jìn)行判定,可快速的判定出所有內(nèi)置類型是否是一個(gè)可迭代對(duì)象或者迭代器對(duì)象:
''' 遇到問題沒人解答?小編創(chuàng)建了一個(gè)Python學(xué)習(xí)交流QQ群:531509025 尋找有志同道合的小伙伴,互幫互助,群里還有不錯(cuò)的視頻學(xué)習(xí)教程和PDF電子書! ''' >>> from collections.abc import Iterable >>> from collections.abc import Iterator>>> isinstance(list(), Iterable) True >>> isinstance(list(), Iterator) False經(jīng)過測(cè)試,所有的容器類型(list、tuple、str、dict、set、frozenset)均屬于可迭代對(duì)象,但不屬于迭代器對(duì)象
原子類型(bool、int、float、None)等均不屬于可迭代對(duì)象,更不屬于迭代器對(duì)象。
也可以通過另一種方式進(jìn)行驗(yàn)證,通過hasattr()函數(shù),檢查類中是否定義了某一個(gè)方法:
>>> hasattr(list,"__iter__") True >>> hasattr(list,"__next__") Falsefor循環(huán)原理
當(dāng)可迭代對(duì)象被for循環(huán)進(jìn)行調(diào)用后,底層執(zhí)行流程如下所示:
1.將自動(dòng)的執(zhí)行iter()方法,該方法內(nèi)部會(huì)查找可迭代對(duì)象的__iter__()方法,如果具有該方法,則返回一個(gè)該可迭代對(duì)象的專屬迭代器對(duì)象,如果沒有該方法,則拋出TypeError object is not iterable的異常。
Ps:每次的for循環(huán)都會(huì)返回一個(gè)全新的迭代器對(duì)象
2.不斷的調(diào)用迭代器對(duì)象的__next__()方法,并且返回迭代器對(duì)象中下一個(gè)數(shù)據(jù)項(xiàng),當(dāng)遍歷完成整個(gè)迭代器后,引發(fā)Stopiteration異常終止迭代
Ps:迭代器本身并不存儲(chǔ)任何數(shù)據(jù)項(xiàng),存儲(chǔ)的只是一個(gè)指針,該指針指向可迭代對(duì)象中真正存儲(chǔ)的數(shù)據(jù)項(xiàng),它指向當(dāng)前被遍歷到的數(shù)據(jù)項(xiàng)索引位置,下一次遍歷則向后推進(jìn)這個(gè)位置
3.for循環(huán)自動(dòng)的捕捉Stopiteration異常,并且停止迭代
Ps:for循環(huán)底層就是while循環(huán)實(shí)現(xiàn)的,只不過多加了3個(gè)步驟:
第一步:執(zhí)行可迭代對(duì)象的__iter()__方法并保存返回的專屬迭代器
第二步:不斷的執(zhí)行迭代器的__next()__方法
第三步:捕獲Stopiteration異常
我們手動(dòng)的實(shí)現(xiàn)一個(gè)for循環(huán):
li1 = list(range(10))iteratorObject = iter(li1) # 1 while 1:try:print(next(iteratorObject)) # 2except StopIteration as e: # 3break1:執(zhí)行可迭代對(duì)象的__iter__()方法并保存返回的專屬迭代器
2:不斷的執(zhí)行迭代器的__next__()方法
3:捕獲Stopiteration異常
線性可迭代對(duì)象與迭代器的實(shí)現(xiàn)
如果是一個(gè)線性容器的可迭代對(duì)象,那么它一定具有索引值,我們可以讓它的__iter__()方法返回一個(gè)專屬的迭代器對(duì)象。
然后專屬迭代器對(duì)象中記錄本次迭代遍歷的索引值,根據(jù)這個(gè)索引值返回可迭代對(duì)象中的數(shù)據(jù)項(xiàng),當(dāng)索引值達(dá)到可迭代對(duì)象中數(shù)據(jù)項(xiàng)總個(gè)數(shù)-1的時(shí)候,拋出異常,本次迭代結(jié)束:
''' 遇到問題沒人解答?小編創(chuàng)建了一個(gè)Python學(xué)習(xí)交流QQ群:531509025 尋找有志同道合的小伙伴,互幫互助,群里還有不錯(cuò)的視頻學(xué)習(xí)教程和PDF電子書! ''' class linearTypeContainer:def __init__(self, array):if isinstance(array, list) or isinstance(array, tuple):self.array = arrayelse:raise TypeError("argument array must is linear container")def __iter__(self):return linearContainer_iterator(self.array) # 1class linearContainer_iterator:def __init__(self, array):self.index = 0self.array = arrayself.len = len(self.array)def __next__(self):if self.index < self.len:retDataItem = self.array[self.index]self.index += 1return retDataItemelse:raise StopIterationdef __iter__(self): # 2return selfcontainer = linearTypeContainer([i for i in range(10)]) for i in container:print(i) print(list(container))1:Python中的一切傳參均為引用傳遞
故linearTypeContainer中的self.array和linearContainer_iterator的self.array都是一個(gè)對(duì)象,并不會(huì)額外開辟內(nèi)存空間
這也就是為什么可迭代對(duì)象創(chuàng)建的專屬迭代器不會(huì)消耗太多的內(nèi)存空間原因了。
2:迭代器對(duì)象一定屬于可迭代對(duì)象范疇,所以在這里我們?yōu)榈鲗?duì)象linearContainer_iterator類也新增了__iter__()方法
這樣做的好處在于如果單獨(dú)的拎出了這個(gè)迭代器對(duì)象,則它也會(huì)支持for循環(huán)的遍歷:
def __iter__(self): # 2return selfcontainerIterator = linearTypeContainer([i for i in range(10)]).__iter__()for item in containerIterator:print(item)# 0 # 1 # 2 # 3 # 4 # 5 # 6 # 7 # 8 # 9如果取消了linearContainer_iterator類的這個(gè)__iter__()方法,則不支持for循環(huán)的遍歷:
...# def __iter__(self): # 2# return selfcontainerIterator = linearTypeContainer([i for i in range(10)]).__iter__()for item in containerIterator:print(item)# TypeError: 'linearContainer_iterator' object is not iterable非線性可迭代對(duì)象與迭代器實(shí)現(xiàn)
如果是一個(gè)非線性容器的可迭代對(duì)象,可以先判斷它的類型,如果傳入的容器是一個(gè)字典,則將迭代的數(shù)據(jù)項(xiàng)集合轉(zhuǎn)換為元組,里面存儲(chǔ)的全部是字典的key即可。
如果傳入的容器是一個(gè)集合,則將迭代的數(shù)據(jù)項(xiàng)集合轉(zhuǎn)換為元組,再參照線性可迭代對(duì)象與迭代器的實(shí)現(xiàn)。
具體實(shí)現(xiàn):
''' 遇到問題沒人解答?小編創(chuàng)建了一個(gè)Python學(xué)習(xí)交流QQ群:531509025 尋找有志同道合的小伙伴,互幫互助,群里還有不錯(cuò)的視頻學(xué)習(xí)教程和PDF電子書! ''' class mappingTypeContainer:def __init__(self, mapping):self.mapping = mappingself.mappingType = Noneif isinstance(mapping, dict):self.mappingType = "dict"elif isinstance(mapping, set) or isinstance(mapping, frozenset):self.mappingType = "set"else:raise TypeError("argument mapping must is mapping container")def keys(self):if self.mappingType == "set":raise TypeError("instance mapping type is set, no have method keys")else:return self.mappingdef values(self):if self.mappingType == "set":raise TypeError("instance mapping type is set, no have method values")else:return self.mapping.values()def items(self):if self.mappingType == "set":raise TypeError("instance mapping type is set, no have method items")else:return self.mapping.items()def __str__(self):return str(self.mapping)def __iter__(self):return mappingContainer_iterator(tuple(self.mapping))class mappingContainer_iterator:def __init__(self, array):self.index = 0self.array = arrayself.len = len(self.array)def __next__(self):if self.index < self.len:retDataItem = self.array[self.index]self.index += 1return retDataItemelse:raise StopIterationdef __iter__(self):return selfcontainer = mappingTypeContainer({str("k") + str(i): str("v") + str(i) for i in range(3)})for item in container.items():print(item)print(container)# ('k0', 'v0') # ('k1', 'v1') # ('k2', 'v2') # {'k0': 'v0', 'k1': 'v1', 'k2': 'v2'}container = mappingTypeContainer({i for i in range(3)})for item in container:print(item)print(container)# 0 # 1 # 2 # {0, 1, 2}迭代器對(duì)象的特性
每一次for循環(huán)創(chuàng)建出的可迭代對(duì)象的專屬迭代器都是一次性的,用完后就沒用了:
# 1 containerIterator = linearTypeContainer([i for i in range(3)]).__iter__()for item in containerIterator:print(item)# 0 # 1 # 2for item in containerIterator:print(item) # 2print("?")1:直接拿出一個(gè)迭代器對(duì)象
2:在第2次循環(huán)中,迭代器對(duì)象中存儲(chǔ)的索引值已經(jīng)最大了,每次調(diào)用iter()都會(huì)拋出異常返回出來再被for處理,所以print()函數(shù)根本不會(huì)運(yùn)行
迭代器對(duì)象并不存儲(chǔ)可迭代對(duì)象中的真正迭代數(shù)據(jù),而是僅存儲(chǔ)長(zhǎng)度和索引,所以內(nèi)存的占用并不多:
''' 遇到問題沒人解答?小編創(chuàng)建了一個(gè)Python學(xué)習(xí)交流QQ群:531509025 尋找有志同道合的小伙伴,互幫互助,群里還有不錯(cuò)的視頻學(xué)習(xí)教程和PDF電子書! ''' class linearContainer_iterator:def __init__(self, array):self.index = 0 # 1self.array = array # 2self.len = len(self.array) # 3...1:占用額外的內(nèi)存空間
2:引用對(duì)象,并不開辟內(nèi)存
3:占用額外的內(nèi)存空間
惰性求值與及早求值
迭代器對(duì)象中對(duì)于返回的數(shù)據(jù)項(xiàng),是進(jìn)行實(shí)時(shí)演算的,這種實(shí)時(shí)演算的特性求值方式被稱為惰性求值,即你需要的時(shí)候我算出來后再給你:
def __next__(self):if self.index < self.len:retDataItem = self.array[self.index]self.index += 1return retDataItemelse:raise StopIteration除開惰性求值,還有一種及早求值的方案,即使你要1個(gè),我也把所有的都給你。
如Python2中的range()、map()、filter()、dict.items()、dict.keys()、dict.values(),它們均返回的是一個(gè)純粹的列表,這樣的設(shè)計(jì)是不合理的。
因?yàn)榉祷氐牧斜頃?huì)占用很大的內(nèi)存空間,而Python3中則統(tǒng)一優(yōu)化為惰性求值方案,即返回一個(gè)可迭代對(duì)象。
要命的問題
①:Python中的所有自帶容器類型為何不自己設(shè)置成迭代器?
而是在for循環(huán)時(shí)實(shí)例出一個(gè)專屬的迭代器?
直接在這些自帶類型的底層實(shí)現(xiàn)__next__()方法不好嗎?
這樣豈不是更加減少了內(nèi)存的消耗,少定義了類和實(shí)例化了類嗎?
答:這真是一個(gè)要命的問題,這個(gè)問題我也想過很久,最后是在stackoverflow提問并且獲得了良好的解答才記錄下來的。
因?yàn)榇_實(shí)是可以實(shí)現(xiàn)的,如下所示,只需要在加上1處代碼即可:
''' 遇到問題沒人解答?小編創(chuàng)建了一個(gè)Python學(xué)習(xí)交流QQ群:531509025 尋找有志同道合的小伙伴,互幫互助,群里還有不錯(cuò)的視頻學(xué)習(xí)教程和PDF電子書! ''' class linearTypeContainer:def __init__(self, array):if isinstance(array, list) or isinstance(array, tuple):self.array = arrayelse:raise TypeError("argument array must is linear container")self.index = 0self.len = len(self.array)def __iter__(self):return selfdef __next__(self):if self.index < self.len:retDataItem = self.array[self.index]self.index += 1return retDataItemelse:self.index = 0 # 1raise StopIterationcontainer = linearTypeContainer(list(range(5)))for item in container:print(item)for item in container:print(item)for item in container:print(item)但是這樣做在某種特殊情況下會(huì)出現(xiàn)問題:
container = linearTypeContainer(list(range(5)))for item in container:print(item)if container.index == 3:breakprint("*"*20)for item in container: print(item)# 0 # 1 # 2 # ******************** # 3 # 4你會(huì)發(fā)現(xiàn)如果第一次for循環(huán)到了1半的時(shí)候退出,第二次for循環(huán)會(huì)接著根據(jù)第一次for循環(huán)進(jìn)行繼續(xù)。
能夠解決一下嗎?只需要加上一個(gè)標(biāo)志位即可:
''' 遇到問題沒人解答?小編創(chuàng)建了一個(gè)Python學(xué)習(xí)交流QQ群:531509025 尋找有志同道合的小伙伴,互幫互助,群里還有不錯(cuò)的視頻學(xué)習(xí)教程和PDF電子書! ''' class linearTypeContainer:def __init__(self, array):if isinstance(array, list) or isinstance(array, tuple):self.array = arrayelse:raise TypeError("argument array must is linear container")self.index = 0self.len = len(self.array)self.iter = False # 1def __iter__(self):if self.iter: # 2self.index = 0self.iter = Truereturn selfdef __next__(self):if self.index < self.len:retDataItem = self.array[self.index]self.index += 1return retDataItemelse:self.index = 0raise StopIterationcontainer = linearTypeContainer(list(range(5)))for item in container:print(item)if container.index == 3:breakprint("*" * 20)for item in container:print(item)# 0 # 1 # 2 # ******************** # 0 # 1 # 2 # 3 # 41:判斷是不是一次新的調(diào)用
2:如果是新的調(diào)用,則將index重新置為0即可
那么為何Python不這樣設(shè)計(jì)呢?我們應(yīng)該更多的考慮多線程的情況下,多個(gè)for循環(huán)使用同一個(gè)迭代器它是否是線程安全的,上面的示例中這個(gè)共享迭代器并不是線程安全的,此外它也不支持嵌套循環(huán),如下所示,這樣會(huì)造成無限循環(huán):
container = linearTypeContainer(list(range(5)))for item in container:print(item)for j in container:print(j)綜上各個(gè)方面的考慮,Python將內(nèi)置的數(shù)據(jù)類型,都設(shè)置了在for循環(huán)時(shí)返回專屬迭代器的做法,這是非常好的設(shè)計(jì),但是對(duì)于有些內(nèi)置的對(duì)象,則是將它本身做成了迭代器,如文件對(duì)象。
②:Python2中返回的及早求值對(duì)象,就沒有一點(diǎn)好處嗎?真的是浪費(fèi)內(nèi)存百無一用?
答:也不是,你可以發(fā)現(xiàn)Python3中所有返回的及早求值對(duì)象,都不支持索引操作,但是Python2中返回的由于是列表,它能夠支持索引操作,在某些極度極端的情況下這確實(shí)是個(gè)優(yōu)勢(shì),但是Python3的惰性求值對(duì)象需要這種優(yōu)勢(shì)嗎?你手動(dòng)將它轉(zhuǎn)換為list不香嗎?這樣提供給了你更多操作性的同時(shí)優(yōu)化了內(nèi)存占用,何樂而不為呢?
③:你能實(shí)現(xiàn)一個(gè)返回惰性求值的對(duì)象嗎?
答:能啊!你看,我實(shí)現(xiàn)一個(gè)Range吧,其實(shí)就是傳參位置和自帶的不一樣,但是它是線程安全的且支持嵌套循環(huán)的:
class Range:def __init__(self, stop, start=0, step=1):self.start = startself.stop = stopself.step = stepself.current = Nonedef __iter__(self):return Range_iterator(self.stop, self.start, self.step)class Range_iterator:def __init__(self, stop, start, step):self.start = startself.stop = stopself.step = stepself.current = self.startdef __next__(self):if self.current < self.stop:retDataItem = self.currentself.current += self.stepreturn retDataItemraise StopIterationfor i in Range(10):print(i)for j in Range(10):print(j)總結(jié)
以上是生活随笔為你收集整理的Python基础教程:迭代器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python分支结构你真的搞定了吗?
- 下一篇: Python基础教程:字符串的常用操作