深度理解Python迭代器
- 2021 年 5 月 20 日
- 筆記
迭代器
迭代是什麼
迭代指的是一個重複的過程,每次重複都必須基於上一次的結果而繼續,單純的重複並不是迭代,如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類進行判定,可快速的判定出所有內置類型是否是一個可迭代對象或者迭代器對象:
>>> 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__")
False
for循環原理
當可迭代對象被for循環進行調用後,底層執行流程如下所示:
-
將自動的執行iter()方法,該方法內部會查找可迭代對象的__iter__()方法,如果具有該方法,則返回一個該可迭代對象的專屬迭代器對象,如果沒有該方法,則拋出TypeError object is not iterable的異常。
Ps:每次的for循環都會返回一個全新的迭代器對象
-
不斷的調用迭代器對象的__next__()方法,並且返回迭代器對象中下一個數據項,當遍歷完成整個迭代器後,引發Stopiteration異常終止迭代
Ps:迭代器本身並不存儲任何數據項,存儲的只是一個指針,該指針指向可迭代對象中真正存儲的數據項,它指向當前被遍歷到的數據項索引位置,下一次遍歷則向後推進這個位置
-
for循環自動的捕捉Stopiteration異常,並且停止迭代
Ps:for循環底層就是while循環實現的,只不過多加了3個步驟:
第一步:執行可迭代對象的__iter()__方法並保存返回的專屬迭代器
第二步:不斷的執行迭代器的__next()__方法
第三步:捕獲Stopiteration異常
我們手動的實現一個for循環:
li1 = list(range(10))
iteratorObject = iter(li1) # ❶
while 1:
try:
print(next(iteratorObject)) # ❷
except StopIteration as e: # ❸
break
❶:執行可迭代對象的__iter__()方法並保存返回的專屬迭代器
❷:不斷的執行迭代器的__next__()方法
❸:捕獲Stopiteration異常
線性可迭代對象與迭代器的實現
如果是一個線性容器的可迭代對象,那麼它一定具有索引值,我們可以讓它的__iter__()方法返回一個專屬的迭代器對象。
然後專屬迭代器對象中記錄本次迭代遍歷的索引值,根據這個索引值返回可迭代對象中的數據項,當索引值達到可迭代對象中數據項總個數-1的時候,拋出異常,本次迭代結束:
class linearTypeContainer:
def __init__(self, array):
if isinstance(array, list) or isinstance(array, tuple):
self.array = array
else:
raise TypeError("argument array must is linear container")
def __iter__(self):
return linearContainer_iterator(self.array) # ❶
class linearContainer_iterator:
def __init__(self, array):
self.index = 0
self.array = array
self.len = len(self.array)
def __next__(self):
if self.index < self.len:
retDataItem = self.array[self.index]
self.index += 1
return retDataItem
else:
raise StopIteration
def __iter__(self): # ❷
return self
container = linearTypeContainer([i for i in range(10)])
for i in container:
print(i)
print(list(container))
❶:Python中的一切傳參均為引用傳遞
故linearTypeContainer中的self.array和linearContainer_iterator的self.array都是一個對象,並不會額外開闢記憶體空間
這也就是為什麼可迭代對象創建的專屬迭代器不會消耗太多的記憶體空間原因了。
❷:迭代器對象一定屬於可迭代對象範疇,所以在這裡我們為迭代器對象linearContainer_iterator類也新增了__iter__()方法
這樣做的好處在於如果單獨的拎出了這個迭代器對象,則它也會支援for循環的遍歷:
def __iter__(self): # ❷
return self
containerIterator = 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): # ❷
# return self
containerIterator = linearTypeContainer([i for i in range(10)]).__iter__()
for item in containerIterator:
print(item)
# TypeError: 'linearContainer_iterator' object is not iterable
非線性可迭代對象與迭代器實現
如果是一個非線性容器的可迭代對象,可以先判斷它的類型,如果傳入的容器是一個字典,則將迭代的數據項集合轉換為元組,裡面存儲的全部是字典的key即可。
如果傳入的容器是一個集合,則將迭代的數據項集合轉換為元組,再參照線性可迭代對象與迭代器的實現。
具體實現:
class mappingTypeContainer:
def __init__(self, mapping):
self.mapping = mapping
self.mappingType = None
if 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.mapping
def 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 = 0
self.array = array
self.len = len(self.array)
def __next__(self):
if self.index < self.len:
retDataItem = self.array[self.index]
self.index += 1
return retDataItem
else:
raise StopIteration
def __iter__(self):
return self
container = 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循環創建出的可迭代對象的專屬迭代器都是一次性的,用完後就沒用了:
# ❶
containerIterator = linearTypeContainer([i for i in range(3)]).__iter__()
for item in containerIterator:
print(item)
# 0
# 1
# 2
for item in containerIterator:
print(item) # ❷
print("?")
❶:直接拿出一個迭代器對象
❷:在第2次循環中,迭代器對象中存儲的索引值已經最大了,每次調用iter()都會拋出異常返回出來再被for處理,所以print()函數根本不會運行
迭代器對象並不存儲可迭代對象中的真正迭代數據,而是僅存儲長度和索引,所以記憶體的佔用並不多:
class linearContainer_iterator:
def __init__(self, array):
self.index = 0 # ❶
self.array = array # ❷
self.len = len(self.array) # ❸
...
❶:佔用額外的記憶體空間
❷:引用對象,並不開闢記憶體
❸:佔用額外的記憶體空間
惰性求值與及早求值
迭代器對象中對於返回的數據項,是進行實時演算的,這種實時演算的特性求值方式被稱為惰性求值,即你需要的時候我算出來後再給你:
def __next__(self):
if self.index < self.len:
retDataItem = self.array[self.index]
self.index += 1
return retDataItem
else:
raise StopIteration
除開惰性求值,還有一種及早求值的方案,即使你要1個,我也把所有的都給你。
如Python2中的range()、map()、filter()、dict.items()、dict.keys()、dict.values(),它們均返回的是一個純粹的列表,這樣的設計是不合理的。
因為返回的列表會佔用很大的記憶體空間,而Python3中則統一優化為惰性求值方案,即返回一個可迭代對象。
要命的問題
①:Python中的所有自帶容器類型為何不自己設置成迭代器?
而是在for循環時實例出一個專屬的迭代器?
直接在這些自帶類型的底層實現__next__()方法不好嗎?
這樣豈不是更加減少了記憶體的消耗,少定義了類和實例化了類嗎?
答:這真是一個要命的問題,這個問題我也想過很久,最後是在stackoverflow提問並且獲得了良好的解答才記錄下來的。
因為確實是可以實現的,如下所示,只需要在加上❶處程式碼即可:
class linearTypeContainer:
def __init__(self, array):
if isinstance(array, list) or isinstance(array, tuple):
self.array = array
else:
raise TypeError("argument array must is linear container")
self.index = 0
self.len = len(self.array)
def __iter__(self):
return self
def __next__(self):
if self.index < self.len:
retDataItem = self.array[self.index]
self.index += 1
return retDataItem
else:
self.index = 0 # ❶
raise StopIteration
container = 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:
break
print("*"*20)
for item in container:
print(item)
# 0
# 1
# 2
# ********************
# 3
# 4
你會發現如果第一次for循環到了1半的時候退出,第二次for循環會接著根據第一次for循環進行繼續。
能夠解決一下嗎?只需要加上一個標誌位即可:
class linearTypeContainer:
def __init__(self, array):
if isinstance(array, list) or isinstance(array, tuple):
self.array = array
else:
raise TypeError("argument array must is linear container")
self.index = 0
self.len = len(self.array)
self.iter = False # ❶
def __iter__(self):
if self.iter: # ❷
self.index = 0
self.iter = True
return self
def __next__(self):
if self.index < self.len:
retDataItem = self.array[self.index]
self.index += 1
return retDataItem
else:
self.index = 0
raise StopIteration
container = linearTypeContainer(list(range(5)))
for item in container:
print(item)
if container.index == 3:
break
print("*" * 20)
for item in container:
print(item)
# 0
# 1
# 2
# ********************
# 0
# 1
# 2
# 3
# 4
❶:判斷是不是一次新的調用
❷:如果是新的調用,則將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 = start
self.stop = stop
self.step = step
self.current = None
def __iter__(self):
return Range_iterator(self.stop, self.start, self.step)
class Range_iterator:
def __init__(self, stop, start, step):
self.start = start
self.stop = stop
self.step = step
self.current = self.start
def __next__(self):
if self.current < self.stop:
retDataItem = self.current
self.current += self.step
return retDataItem
raise StopIteration
for i in Range(10):
print(i)
for j in Range(10):
print(j)