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