關於python中迭代器,生成器介紹的文章不算少數,有些寫的也很透徹,但是更多的是碎片化的內容。本來可迭代物件、迭代器、生成器概念就很繞,又加上過於碎片的內容,更讓人摸不著頭腦。本篇嘗試用系統的介紹三者的概念和關係,希望能夠幫助需要的人。
可迭代物件、迭代器
概念簡介
迭代
:
首先看迭代的字面意思:
迭代的意思就是:迭代是一種行為,反覆執行的動作。在python中可以理解為反覆取值的動作。
可迭代物件
:顧名思義就是可以從裡面迭代取值的物件,在python中容器類的資料結構都是可迭代物件,如列表,字典,集合,元組等。
迭代器
:類似於從可迭代物件中取值的一種工具,嚴謹的說可以將可迭代物件中的值取出的物件。
可迭代物件
在python中,容器型別的資料結構都是可迭代物件,列舉如下:
- 列表
- 字典
- 元組
- 集合
- 字串
西遊記第一天團人物列表:
>>> arr = ['聖僧','大聖','天蓬','捲簾']
>>> for i in arr:
... print(i)
...
聖僧
大聖
天蓬
捲簾
>>>
除了python自帶的資料結構是可迭代物件之外,模組裡的方法、自定義的類也可能是可迭代物件。那麼如何確認一個物件是否為可迭代物件呢?有一個標準,那就是可迭代物件都有方法__iter__
,凡是具有該方法的物件都是可迭代物件。
>>> dir(arr)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
迭代器
常見迭代器是從可迭代物件建立而來。呼叫可迭代物件的__iter__
方法就可以為該可迭代物件建立其專屬迭代器。使用iter()
方法也可以建立迭代器,iter()
本質上就是呼叫可迭代物件的__iter__
方法。
>>> arr = ['聖僧','大聖','天蓬','捲簾']
>>> arr_iter = iter(arr)
>>>
>>> for i in arr_iter:
... print(i)
...
聖僧
大聖
天蓬
捲簾
>>>
>>>
>>> arr_iter = iter(arr)
>>> next(arr_iter)
'聖僧'
>>> next(arr_iter)
'大聖'
>>> next(arr_iter)
'天蓬'
>>> next(arr_iter)
'捲簾'
>>> next(arr_iter)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
可迭代物件只能通過for迴圈來遍歷,而迭代器除了可以通過for迴圈來遍歷,重要的是還可以通過next()
方法來迭代出元素。呼叫一次迭代出一個元素,直到所有元素都迭代完,丟擲StopIteration
錯誤。這個過程就像象棋中沒有過河的小卒子——只能前進不能後退,並且迭代完所有元素也無法再次遍歷。
簡單總結迭代器的特徵:
- 可以使用
next()
方法迭代取值 - 迭代的過程只能向前不能後退
- 迭代器是一次性的,迭代完所有元素就無法再次遍歷,需要再次遍歷只有新建迭代器
迭代器物件在python中很常見,比如開啟的檔案就是一個迭代器、map,filter,reduce等高階函式的返回也是迭代器。迭代器物件擁有兩個方法:__iter__
和__next__
。next()
方法能迭代出元素就是呼叫__next__
來實現的。
>>> dir(arr_iter)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__']
區分可迭代物件和迭代器
如何區分可迭代物件和迭代器呢?在python的資料型別增強模組collections
中有可迭代物件和迭代器資料型別,通過isinstance
型別比較即可區分出兩者。
>>> from collections import Iterable, Iterator
>>> arr = [1,2,3,4]
>>> isinstance(arr, Iterable)
True
>>> isinstance(arr, Iterator)
False
>>>
>>>
>>> arr_iter = iter(arr)
>>> isinstance(arr_iter, Iterable)
True
>>> isinstance(arr_iter, Iterator)
True
>>>
arr:可迭代物件。是可迭代物件型別,不是迭代器型別
arr_iter:迭代器。既是可迭代物件型別,又是迭代器型別
可迭代物件和迭代器的關係
從迭代器的建立就能大致看出。可迭代物件就是一個集合,而迭代器就是為這個集合建立的迭代方法。迭代器迭代時是直接從可迭代物件集合裡取值。可以用如下模型來理解兩者之間的關係:
>>> arr = [1,2,3,4]
>>> iter_arr = iter(arr)
>>>
>>> arr.append(100)
>>> arr.append(200)
>>> arr.append(300)
>>>
>>> for i in iter_arr:
... print(i)
...
1
2
3
4
100
200
300
>>>
可以看到這裡的流程是:
- 先建立可迭代物件arr
- 然後從arr建立的arr_iter迭代器
- 再向arr列表追加元素
- 最後迭代出來的元素包括後追加的元素。
可以說明迭代器並不是copy了可迭代物件的元素,而是引用了可迭代物件的元素。在迭代取值時直接使用了可迭代物件的元素。
可迭代物件和迭代器的工作機制
首先整理一下兩者的方法
可迭代物件: 物件中有__iter__
方法
迭代器:物件中有__iter__
和 __next__
方法
在迭代器的建立時提到過__iter__
方法是返回一個迭代器,__next__
是從元素中取值。所以,關於兩者方法的功能:
可迭代物件:
__iter__
方法的作用是返回一個迭代器
迭代器:
__iter__
方法的作用是返回一個迭代器,就是自己。
__next__
方法的作用是返回集合中下一個元素
可迭代物件是一個元素集合,本身沒有自帶取值的方法,可迭代物件就像老話說的茶壺裡的餃子,有貨倒不出。
>>> arr = [1,2,3,4]
>>>
>>> next(arr)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'list' object is not an iterator
既然餃子倒不出來,又想吃怎麼辦?那就得找筷子一樣的工具來夾出來對吧。而迭代器就是給用來給可迭代物件取值的工具。
給可迭代物件arr建立的迭代器arr_iter,可以通過next取值,將arr中值全部迭代出來,直到沒有元素丟擲異常StopIteration
>>> arr_iter = iter(arr)
>>>
>>> next(arr_iter)
1
>>> next(arr_iter)
2
>>> next(arr_iter)
3
>>> next(arr_iter)
4
>>> next(arr_iter)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
for 迴圈本質
>>> arr = [1,2,3]
>>> for i in arr:
... print(i)
...
1
2
3
以上通過for迴圈遍歷出arr中所有值。我們知道列表arr是可迭代物件,本身無法取值,for迴圈如何迭代出所有元素呢?
for迴圈的本質就是給arr建立一個迭代器,然後不斷呼叫next()方法取出元素,複製給變數i,直到沒有元素丟擲捕獲StopIteration的異常,退出迴圈。可以通過模擬for迴圈更直觀的說明:
arr = [1,2,3]
# 給arr生成一個迭代器
arr_iter = iter(arr)
while True:
try:
# 不斷呼叫迭代器next方法,並捕獲異常,然後退出
print(next(arr_iter))
except StopIteration:
break
>>
1
2
3
到這裡大概就講完了可迭代物件和迭代器的工作機制,簡單總結:
可迭代物件: 儲存元素,但自身無法取值。可以呼叫自己的__iter__
方法建立一個專屬迭代器來取值。
迭代器:擁有__next__
方法,可以從指向的可迭代物件中取值。只能遍歷一遍,並且只能前進不能後退。
自己動手建立可迭代物件和迭代器
榴蓮好不好吃,只有嘗一嘗才知道。迭代器好不好理解,動手實現一次就清楚。下面自定義可迭代物件和迭代器。
如果自定義一個可迭代物件,那麼需要實現__iter__
方法;
如果要自定義一個迭代器,那麼就需要實現__iter__
和__next__
方法。
可迭代物件:實現__iter__
方法,功能是呼叫該方法返回迭代器
迭代器:實現__iter__
,功能是返回迭代器,也就是自身;實現__next__
,功能是迭代取值直到丟擲異常。
from collections import Iterable, Iterator
# 可迭代物件
class MyArr():
def __init__(self):
self.elements = [1,2,3]
# 返回一個迭代器,並將自己元素的引用傳遞給迭代器
def __iter__(self):
return MyArrIterator(self.elements)
# 迭代器
class MyArrIterator():
def __init__(self, elements):
self.index = 0
self.elements = elements
# 返回self,self就是例項化的物件,也就是呼叫者自己。
def __iter__(self):
return self
# 實現取值
def __next__(self):
# 迭代完所有元素丟擲異常
if self.index >= len(self.elements):
raise StopIteration
value = self.elements[self.index]
self.index += 1
return value
arr = MyArr()
print(f'arr 是可迭代物件:{isinstance(arr, Iterable)}')
print(f'arr 是迭代器:{isinstance(arr, Iterator)}')
# 返回了迭代器
arr_iter = arr.__iter__()
print(f'arr_iter 是可迭代物件:{isinstance(arr_iter, Iterable)}')
print(f'arr_iter 是迭代器:{isinstance(arr_iter, Iterator)}')
print(next(arr_iter))
print(next(arr_iter))
print(next(arr_iter))
print(next(arr_iter))
結果:
arr 是可迭代物件:True
arr 是迭代器:False
arr_iter 是可迭代物件:True
arr_iter 是迭代器:True
1
2
3
Traceback (most recent call last):
File "myarr.py", line 40, in <module>
print(next(arr_iter))
File "myarr.py", line 23, in __next__
raise StopIteration
StopIteration
從這個列子就能清晰的認識可迭代物件的迭代器的實現。可迭代物件的__iter__
方法返回值就是一個例項化的迭代器的物件。這個迭代器的物件儲存了可迭代物件的元素的引用,也實現了取值的方法,所以可以通過next()
方法取值。這是一個值得細品的程式碼,比如說有幾個問題可以留給讀者思考:
- 為什麼next()只能前進不能後退
- 為什麼迭代器只能遍歷一次就失效
- 如果for迴圈的目標是迭代器,工作機制是怎樣
迭代器的優勢
設計模式之迭代模式
迭代器的優勢是:提供了一種通用不依賴索引的迭代取值方式
迭代器的設計來源於設計模式之迭代模式
。迭代模式的思想是:提供一種方法順序地訪問一個容器中的元素,而又不需要暴露該物件的內部細節。
迭代模式具體到python的迭代器中就是能夠將遍歷序列的操作和序列底層相分離,提供一種通用的方法去遍歷元素。
如列表、字典、集合、元組、字串。這些資料結構的底層資料模型都不一樣,但是同樣都可以使用for迴圈來遍歷。正是因為每一種資料結構都可以生成迭代器,都可以通過next()方法迭代,所以在使用的時候不需要關心元素的在底層如何儲存,不需要考慮內部細節。
同樣如果是自定的資料型別,即使是內部實現比較複雜,只需要實現迭代器,也就不需要關心複雜的結構,使用通用的next方法即可遍歷元素。
複雜資料結構的通用取值實現
比如我們構造一個複雜的資料結構:{(x,x):value}
,一個字典,key是元組,value是數字。按照迭代的設計模式,實現通用取值方法。
例子實現
:
class MyArrIterator():
def __init__(self):
self.index = 1
self.elements = {(1,1):100, (2,2):200, (3,3):300}
def __iter__(self):
return self
def __next__(self):
if self.index > len(self.elements):
raise StopIteration
value = self.elements[(self.index, self.index)]
self.index += 1
return value
arr_iter = MyArrIterator()
print(next(arr_iter))
print(next(arr_iter))
print(next(arr_iter))
print(next(arr_iter))
100
200
300
Traceback (most recent call last):
File "iter_two.py", line 22, in <module>
print(next(arr_iter))
File "iter_two.py", line 12, in __next__
raise StopIteration
StopIteration
只要實現了__next__
方法就可以通過next()取值,不管資料結構多麼複雜,__next__
遮蔽了底層細節。這種設計思想是一個比較常見的思想,比如驅動設計,第三方平臺介入設計等都是遮蔽差異,提供一個統一的呼叫方法。
迭代器的缺點和誤區
缺點
在上面的介紹中也提到了迭代器的缺點,集中說一下:
- 取值不夠靈活。next方法只能往後取值,不能往前。取值不如按照索引的方式靈活,不能取指定的某一個值
- 無法預測迭代器的長度。迭代器通過next()方法取值,並不能提前知道要迭代出的個數
- 用完一次就失效
誤區
迭代器的優勢和缺點已經說的清晰了,現在討論一個普遍對迭代器的一個誤區:迭代器是不能節省記憶體的
給這句話加一個前提:這裡的迭代器是指普通的迭代器,而非生成器,因為生成器也是一種特殊的迭代器。
這可能是一個認識的誤區,認為建立一個功能相同的可迭代物件和迭代器,迭代器的記憶體佔用小於可迭代物件。例如:
>>> arr = [1,2,3,4]
>>> arr_iter = iter([1,2,3,4])
>>>
>>> arr.__sizeof__()
72
>>> arr_iter.__sizeof__()
32
咋一看確實是迭代器佔用的記憶體小於可迭代物件,可仔細想一下迭代器的實現,它是引用了可迭代物件的元素,也就是說建立迭代器arr_iter同時也建立了一個列表[1,2,3,4],迭代器只是儲存了列表的引用,所以迭代器的arr_iter實際的記憶體是[1,2,3,4] + 32= 72 + 32 = 104位元組。
arr_iter
本質上是一個類的物件,因為python變數是儲存地址的特性,所以物件的的地址大小都是32位元組。
後面有專門關於迭代器和生成器佔用記憶體的分析,能夠用數字來證明這個觀點。
python自帶的迭代器工具itertools
迭代器在python佔有重要的位置,所以python內建了迭代器功能模組itertools
。itertools中所有的方法都是迭代器,可以使用next()取值。方法主要可以分為三類,分別是無限迭代器,有限迭代器,組合迭代器
無限迭代器
count():建立一個無限的迭代器,類似於無限長度的列表,可以從中取值
有限迭代器
chain():可以把多個可迭代物件組合起來,形成一個更大的迭代器
組合迭代器
product():得到的是可迭代物件的笛卡兒積
關於更多itertools的使用可參考:https://zhuanlan.zhihu.com/p/51003123
生成器
生成器是一種特殊的迭代器,它既具有迭代器的功能:能夠通過next方法迭代出元素,又有自己的特殊之處:節省記憶體。
生成器的建立方法
生成器有兩種建立方法,分別是:
()
語法,將列表生成式的[]
換成()
就可以建立生成器- 使用
yield
關鍵字將普通函式變成生成器函式
()語法
>>> gen = (i for i in range(3))
>>> type(gen)
<class 'generator'>
>>> from collections import Iterable,Iterator
>>>
>>> isinstance(gen, Iterable)
True
>>> isinstance(gen, Iterator)
True
>>>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
可以看到生成器符合迭代器的特徵。
yield 關鍵字
yield
是python的關鍵字,在函式中使用yield就能將普通函式變成生成器。函式中return是返回標識,程式碼執行到return就退出了。而程式碼執行到yield時也會返回yield後面的變數,但是程式只是暫停在當前位置,當再次執行程式時會從yield之後的部分開始執行。
from collections import Iterator,Iterable
def fun():
a = 1
yield a
b = 100
yield b
gen_fun = fun()
print(f'是可迭代物件:{isinstance(gen_fun, Iterable)}')
print(f'是迭代器:{isinstance(gen_fun, Iterator)}')
print(next(gen_fun))
print(next(gen_fun))
print(next(gen_fun))
是可迭代物件:True
是迭代器:True
1
100
Traceback (most recent call last):
File "gen_fun.py", line 17, in <module>
print(next(gen_fun))
StopIteration
執行第一個next()時,程式通過yield a返回了1,執行流程就暫停在這裡。
執行第二個next()時,程式從上次暫停的地方開始執行,然後通過yield b返回了100,最後退出,程式結束。
yield的魔力就是能夠記住執行位置,並且能夠從執行位置再次執行下去。
生成器方法
生成器既然是一種特殊的迭代器,那麼是否具有迭代器物件的兩個方法呢?檢視兩種生成器擁有的方法。
gen
>>> dir(gen)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
gen_fun
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
兩種生成器都有用迭代器的__iter__
和__next__
方法。
生成器是特殊的迭代器,想要區分出生成器和迭代器就不能使用collections
的Iterator
了。可以使用isgenerator
方法:
>>> from inspect import isgenerator
>>> arr_gen = (i for i in range(10))
>>> isgenerator(arr_gen)
True
>>>
>>> arr = [i for i in range(10)]
>>> isgenerator(arr)
False
>>>
生成器的優勢
生成器是一種特殊的迭代器,它的特殊之處就是它的優勢:節省記憶體。從名字就可以看出,生成器,通過生成的方法來支援迭代取值。
節省記憶體原理:
以遍歷列表為例,列表元素按照某種演算法推算出來,就可以在迴圈的過程中不斷推算出後續的元素,這樣就不必建立完整的列表,從而節省大量的空間。
以實現同樣的的功能為例,迭代出集合中的元素。集合為:[1,2,3,4]
迭代器的做法:
- 首先生成一個可迭代物件,列表[1,2,3,4]
- 然後建立迭代器,從可迭代物件中通過next()取值
arr = [1,2,3,4]
arr_iter = iter(arr)
next(arr_iter)
next(arr_iter)
next(arr_iter)
next(arr_iter)
生成器的做法:
- 建立一個生成器函式
- 通過next取值
def fun():
n = 1
while n <= 4:
yield n
n += 1
gen_fun = fun()
print(next(gen_fun))
print(next(gen_fun))
print(next(gen_fun))
print(next(gen_fun))
比較這兩種方法,迭代器需要建立一個列表來完成迭代,而生成器只需要一個數字就可以完成迭代。在資料量小的情況下還不能體現這個優勢,當資料量巨大時這個優勢能展現的淋漓盡致。比如同樣生成10w個數字,迭代器需要10w個元素的列表,而生成器只需要一個元素。當然就能節省記憶體。
生成器是一種以時間換空間的做法,迭代器是從已經在記憶體中建立好的集合中取值,所以消耗記憶體空間,而生成器只儲存一個值,取一次值就計算一次,消耗cpu但節省記憶體空間。
生成器應用場景
- 資料的資料規模巨大,記憶體消耗嚴重
- 數列有規律,但是依靠列表推導式描述不出來
- 協程。生成器和協程有著千絲萬縷的聯絡
生成器節省記憶體、迭代器不節省記憶體
實踐是檢驗真理的唯一標準,通過記錄記憶體的變化來檢測迭代器和生成器哪個能夠節省記憶體。
環境:
系統
:Linux deepin 20.2.1
記憶體
:8G
python版本
: 3.7.3
記憶體監控工具
: free -b
以位元組為單位的記憶體展示
方法
:生成100萬規模的列表,從0到100w,對比生成資料前後的記憶體變化
可迭代物件
>>> arr = [i for i in range(1000000)]
>>>
>>> arr.__sizeof__()
8697440
>>>
第一次free -b
在生成列表之前;第二次在生成列表之後。下同
ljk@work:~$ free -b
total used free shared buff/cache available
Mem: 7978999808 1424216064 2386350080 362094592 4168433664 5884121088
Swap: 0 0 0
ljk@work:~$ free -b
total used free shared buff/cache available
Mem: 7978999808 1464410112 2352287744 355803136 4162301952 5850210304
Swap: 0 0 0
現象:記憶體增加:從1424216064
位元組增加1464410112字
節,增加 38.33
MB
迭代器
>>> a = iter([i for i in range(1000000)])
>>>
>>> a.__sizeof__()
32
ljk@work:~$ free -b
total used free shared buff/cache available
Mem: 7978999808 1430233088 2385924096 355160064 4162842624 5885038592
Swap: 0 0 0
ljk@work:~$ free -b
total used free shared buff/cache available
Mem: 7978999808 1469304832 2346835968 355160064 4162859008 5845966848
Swap: 0 0 0
現象:記憶體增加:從1430233088
位元組增加1469304832
節,增加 37.26
MB
生成器
>>> arr = (i for i in range(1000000))
>>>
>>> arr.__sizeof__()
96
>>>
ljk@work:~$ free -b
total used free shared buff/cache available
Mem: 7978999808 1433968640 2373222400 362868736 4171808768 5873594368
Swap: 0 0 0
ljk@work:~$ free -b
total used free shared buff/cache available
Mem: 7978999808 1434963968 2378940416 356118528 4165095424 5879349248
Swap: 0 0 0
現象:記憶體增加:從1433968640
位元組增加1434963968
節,增加 0.9492
MB
小結:
- | 系統記憶體 | 變數記憶體 |
---|---|---|
可迭代物件 | 38.33MB | 8.29MB |
迭代器 | 37.26MB | 32k |
生成器 | 0.9492MB | 96k |
以上結論經過多次實現,基本儲存變數一致。從資料結果來看迭代器不能節省記憶體,生成器可以節省記憶體。生成100w規模的資料,迭代器的記憶體消耗是生成器的40倍左右,結果存在一定誤差。
總結
可迭代物件:
屬性
:一種容器物件
特點
:能夠儲存元素集合,自己無法實現迭代取值,在外界的幫助下可以迭代取值
特徵
:有__iter__
方法
迭代器:
屬性
:一種工具物件
特點
:可以實現迭代取值,取值的來源是可迭代物件儲存的集合
特徵
:有__iter__
和__next__
方法
優點
:實現通用的迭代方法
生成器:
屬性
:一種函式物件
特點
:可以實現迭代取值,只儲存一個值,通過計算返回迭代的下一個值。以計算換記憶體。
特徵
:有__iter__
和__next__
方法
優點
:擁有迭代器特點同時能夠節省記憶體
關於可迭代物件
、迭代器
、生成器
的內容講的比較多,不知道讀者是不是已經雲裡霧裡了?出道題檢驗一下:西遊記第一天團的人物名字是以誰的視角來稱呼的?
參考:
https://zhuanlan.zhihu.com/p/71703028
https://blog.csdn.net/mpu_nice/article/details/107299963