Python中for循環(huán)可迭代對(duì)象迭代器及生成器源碼學(xué)習(xí)
問(wèn)題:
之前在學(xué)習(xí)list和dict相關(guān)的知識(shí)時(shí),遇到了一個(gè)常見(jiàn)的問(wèn)題:如何在遍歷list或dict的時(shí)候正常刪除?例如我們?cè)诒闅vdict的時(shí)候刪除,會(huì)報(bào)錯(cuò):RuntimeError: dictionary changed size during iteration;而在遍歷list的時(shí)候刪除,會(huì)有部分元素刪除不完全。
由這個(gè)問(wèn)題又引發(fā)了我對(duì)另一個(gè)問(wèn)題的思考:我們通過(guò)for循環(huán)去遍歷一個(gè)list或dict時(shí),具體是如何for的呢?即for循環(huán)的本質(zhì)是什么?
在查閱了相關(guān)資料后,我認(rèn)識(shí)到這是一個(gè)和迭代器相關(guān)的問(wèn)題,所以借此機(jī)會(huì)來(lái)詳細(xì)認(rèn)識(shí)一下Python中的for循環(huán)、可迭代對(duì)象、迭代器和生成器
1. 迭代
“迭代是重復(fù)反饋過(guò)程的活動(dòng),其目的通常是為了逼近所需目標(biāo)或結(jié)果。”在Python中,可迭代對(duì)象、迭代器、for循環(huán)都是和“迭代”密切相關(guān)的知識(shí)點(diǎn)。
1.1 可迭代對(duì)象Iterable
在Python中,稱可以迭代的對(duì)象為可迭代對(duì)象。要判斷一個(gè)類(lèi)是否可迭代,只需要判斷這個(gè)類(lèi)是否為Iterable類(lèi)的實(shí)例即可:
>>> from collections.abc import Iterable >>> isinstance([], Iterable) True >>> isinstance(123, Iterable) False
上述提供了一個(gè)判斷對(duì)象是否為可迭代對(duì)象的方法,那么一個(gè)對(duì)象怎么才是可迭代對(duì)象呢——只需要該對(duì)象的類(lèi)實(shí)現(xiàn)了__iter__()方法即可:
>>> class A: pass >>> isinstance(A(), Iterable) False >>> class B: def __iter__(self): pass >>> isinstance(B(), Iterable) True
由此可見(jiàn),只要一個(gè)類(lèi)實(shí)現(xiàn)了__iter__()方法,那么這個(gè)類(lèi)的實(shí)例對(duì)象就是可迭代對(duì)象。注意這里的__iter__()方法可以沒(méi)有任何內(nèi)容。
1.2 迭代器Iterator
在Python中,通過(guò)Iterator類(lèi)與迭代器相對(duì)應(yīng)。相較于可迭代對(duì)象,迭代器只是多實(shí)現(xiàn)了一個(gè)__next__()方法:
>>> from collections.abc import Iterator >>> class C: def __iter__(self): pass def __next__(self): pass >>> isinstance(C(), Iterator) True
顯然,迭代器一定是可迭代對(duì)象(因?yàn)榈魍瑫r(shí)實(shí)現(xiàn)了__iter__()方法和__next__()方法),而可迭代對(duì)象不一定是迭代器。
我們來(lái)看一下內(nèi)建類(lèi)型中的可迭代對(duì)象是否為迭代器:
>>> isinstance(C(), Iterator) True >>> isinstance([], Iterable) True >>> isinstance([], Iterator) False >>> isinstance('123', Iterable) True >>> isinstance('123', Iterator) False >>> isinstance({}, Iterable) True >>> isinstance({}, Iterator) False
由此可見(jiàn),str、list、dict對(duì)象都是可迭代對(duì)象,但它們都不是迭代器。
至此,我們對(duì)可迭代對(duì)象和迭代器有了一個(gè)基本概念上的認(rèn)識(shí),也知道了有__iter__()和__next__()這兩種方法。但是這兩個(gè)魔法方法究竟是如何使用的呢?它們和for循環(huán)又有什么關(guān)系呢?
1.3 for循環(huán)
1.3.1 iter()方法和next()方法
iter()方法和next()方法都是Python提供的內(nèi)置方法。對(duì)對(duì)象使用iter()方法會(huì)調(diào)用對(duì)象的__iter__()方法,對(duì)對(duì)象使用next()方法會(huì)調(diào)用對(duì)象的__next__()方法。下面我們具體看一下它們之間的關(guān)系。
1.3.2 iter()和__iter__()
__iter__()方法的作用就是返回一個(gè)迭代器,一般我們可以通過(guò)內(nèi)置函數(shù)iter()來(lái)調(diào)用對(duì)象的__iter__()方法
1.2中舉的例子,只是簡(jiǎn)單的實(shí)現(xiàn)了__iter__()方法,但函數(shù)體直接被pass掉了,本質(zhì)上是沒(méi)有實(shí)現(xiàn)迭代功能的,現(xiàn)在我們來(lái)看一下__iter__()正常使用時(shí)的例子:
>>> class A: def __iter__(self): print('執(zhí)行A類(lèi)的__iter__()方法') return B() >>> class B: def __iter__(self): print('執(zhí)行B類(lèi)的__iter__()方法') return self def __next__(self): pass >>> a = A() >>> a1 = iter(a) 執(zhí)行A類(lèi)的__iter__()方法 >>> b = B() >>> b1 = iter(b) 執(zhí)行B類(lèi)的__iter__()方法
可以看到,對(duì)于類(lèi)A,我們?yōu)樗腳_iter__()方法設(shè)置了返回值為B(),而B(niǎo)()就是一個(gè)迭代器;
而對(duì)于類(lèi)B,我們?cè)谒腳_iter__()方法中直接返回了它的實(shí)例self,因?yàn)樗膶?shí)例本身就是可迭代對(duì)象。
當(dāng)然這里我們也可以返回其他的迭代器,但是如果__iter__()方法返回的是一個(gè)非迭代器,那么當(dāng)我們調(diào)用iter()方法時(shí)就會(huì)報(bào)錯(cuò):
>>> class C: def __iter__(self): pass >>> iter(C()) Traceback (most recent call last): File "<pyshell#4>", line 1, in <module> iter(C()) TypeError: iter() returned non-iterator of type 'NoneType' >>> class D: def __iter__(self): return [] >>> iter(D()) Traceback (most recent call last): File "<pyshell#8>", line 1, in <module> iter(D()) TypeError: iter() returned non-iterator of type 'list'
1.3.3 next()和__next__()
__next__()方法的作用是返回遍歷過(guò)程中的下一個(gè)元素,如果沒(méi)有下一個(gè)元素,則會(huì)拋出StopIteration異常,一般我們可以通過(guò)內(nèi)置函數(shù)next()來(lái)調(diào)用對(duì)象的__next__()方法
下面我們以list對(duì)象為例,來(lái)看一下next是如何遍歷的:
>>> l1 = [1, 2, 3] >>> next(l1) Traceback (most recent call last): File "<pyshell#1>", line 1, in <module> next(l1) TypeError: 'list' object is not an iterator
可以看到,當(dāng)我們直接對(duì)列表對(duì)象l1使用next()方法時(shí),會(huì)報(bào)錯(cuò)’list’ object is not an iterator,顯然list對(duì)象并不是迭代器,也就是說(shuō)它沒(méi)有實(shí)現(xiàn)__next__()方法,那么我們?cè)趺床拍苋?rdquo;對(duì)一個(gè)列表對(duì)象使用next()“呢——根據(jù)我們前面介紹的__iter__()方法,我們知道它會(huì)返回一個(gè)迭代器,而迭代器是實(shí)現(xiàn)了__next__()方法的,所以我們可以先對(duì)list對(duì)象使用iter__(),獲取到它對(duì)應(yīng)的迭代器,然后對(duì)這個(gè)迭代器使用next()就可以了:
>>> l1 = [1, 2, 3] >>> l1_iter = iter(l1) >>> type(l1_iter) <class 'list_iterator'> >>> next(l1_iter) 1 >>> next(l1_iter) 2 >>> next(l1_iter) 3 >>> next(l1_iter) Traceback (most recent call last): File "<pyshell#6>", line 1, in <module> next(l1_iter) StopIteration
思考:__next__()為什么要不停地去取出元素,并且在最后去拋出異常,而不是通過(guò)對(duì)象的長(zhǎng)度相關(guān)信息來(lái)確定調(diào)用次數(shù)?
個(gè)人認(rèn)為是因?yàn)槲覀兛梢酝ㄟ^(guò)next()去手動(dòng)調(diào)用對(duì)象的__next__()方法,而在next()中并沒(méi)有判斷對(duì)象的長(zhǎng)度,所以需要在__next__()去處理
1.3.4 自定義類(lèi)實(shí)現(xiàn)__iter__()和__next__()
下面我們?cè)囍ㄟ^(guò)實(shí)現(xiàn)自定義一下list的迭代過(guò)程:
首先我們定義一個(gè)類(lèi)A,它是一個(gè)可迭代對(duì)象,__iter__()方法會(huì)返回一個(gè)迭代器B(),并且還擁有一個(gè)成員變量m_Lst:
>>> class A: def __init__(self, lst): self.m_Lst = lst def __iter__(self): return B(self.m_Lst)
對(duì)于迭代器的類(lèi)B,我們實(shí)現(xiàn)它的__iter__()方法和__next__()方法,注意在__next__()方法中我們需要拋出StopIteration異常。此外,它擁有兩個(gè)成員變量self.m_Lst和self.m_Index用于迭代遍歷:
>>> class B: def __init__(self, lst): self.m_Lst = lst self.m_Index= 0 def __iter__(self): return self def __next__(self): try: value = self.m_Lst[self.m_Index] self.m_Index += 1 return value except IndexError: raise StopIteration()
至此,我們已經(jīng)完成了迭代器的準(zhǔn)備工作,下面我們來(lái)實(shí)踐一下迭代吧,為了更好地展示這個(gè)過(guò)程,我們可以加上一些打?。?/p>
>>> class A: def __init__(self, lst): self.m_Lst = lst def __iter__(self): print('call A().__iter__()') return B(self.m_Lst) >>> class B: def __init__(self, lst): self.m_Lst = lst self.m_Index= 0 def __iter__(self): print('call B().__iter__()') return self def __next__(self): print('call B().__next__()') try: value = self.m_Lst[self.m_Index] self.m_Index += 1 return value except IndexError: print('call B().__next__() except IndexError') raise StopIteration() >>> l = [1, 2, 3] >>> a = A(l) >>> a_iter = iter(a) call A().__iter__() >>> next(a_iter) call B().__next__() 1 >>> next(a_iter) call B().__next__() 2 >>> next(a_iter) call B().__next__() 3 >>> next(a_iter) call B().__next__() call B().__next__() except IndexError Traceback (most recent call last): File "<pyshell#5>", line 11, in __next__ value = self.m_Lst[self.m_Index] IndexError: list index out of range During handling of the above exception, another exception occurred: Traceback (most recent call last): File "<pyshell#12>", line 1, in <module> next(a_iter) File "<pyshell#5>", line 16, in __next__ raise StopIteration() StopIteration
可以看到,我們借助iter()和next()方法能夠很好地將整個(gè)遍歷的過(guò)程展示出來(lái)。至此,我們對(duì)可迭代對(duì)象、迭代器以及__iter__()和__next__()都有了一定的認(rèn)識(shí),那么,for循環(huán)和它們有什么關(guān)系呢?
1.3.5 探究for循環(huán)
for循環(huán)是我們使用頻率最高的操作之一,我們一般會(huì)用它來(lái)遍歷一個(gè)容器(列表、字典等),這些容器都有一個(gè)共同的特點(diǎn)——都是可迭代對(duì)象。那么對(duì)于我們自定義的類(lèi)A,它的實(shí)例對(duì)象a應(yīng)該也可以通過(guò)for循環(huán)來(lái)遍歷:
>>> for i in a: print(i) call A().__iter__() call B().__next__() 1 call B().__next__() 2 call B().__next__() 3 call B().__next__() call B().__next__() except IndexError >>> for i in a: pass call A().__iter__() call B().__next__() call B().__next__() call B().__next__() call B().__next__() call B().__next__() except IndexError
通過(guò)打印,我們可以清楚的看到:對(duì)一個(gè)可迭代對(duì)象使用for循環(huán)進(jìn)行遍歷時(shí),for循環(huán)會(huì)調(diào)用該對(duì)象的__iter__()方法來(lái)獲取到迭代器,然后循環(huán)調(diào)用該迭代器的__next__()方法,依次獲取下一個(gè)元素,并且最后會(huì)捕獲StopIteration異常(這里可以嘗試在類(lèi)B的__next__()方法最后只捕獲IndexError而不拋出StopIteration,則for循環(huán)此時(shí)會(huì)無(wú)限循環(huán))
既然我們提到了for循環(huán)會(huì)自動(dòng)去捕獲StopIteration異常,當(dāng)沒(méi)有捕獲到StopIteration異常時(shí)會(huì)無(wú)限循環(huán),那么我們是否可以用while循環(huán)來(lái)模擬一下這個(gè)過(guò)程呢?
>>> while True: try: i = next(a_iter) print(i) except StopIteration: print('except StopIteration') break call B().__next__() 1 call B().__next__() 2 call B().__next__() 3 call B().__next__() call B().__next__() except IndexError except StopIteration
到這里,大家應(yīng)該對(duì)for對(duì)可迭代對(duì)象遍歷的過(guò)程有了一定的了解,想要更深入了解的話可以結(jié)合源碼進(jìn)一步學(xué)習(xí)(本次學(xué)習(xí)分享主要是結(jié)合實(shí)際代碼對(duì)一些概念進(jìn)行講解,并未涉及到相應(yīng)源碼)。
2 生成器
迭代器和生成器總是會(huì)被同時(shí)提起,那么它們之間有什么關(guān)聯(lián)呢——生成器是一種特殊的迭代器。
2.1 獲取生成器
當(dāng)一個(gè)函數(shù)體內(nèi)使用yield關(guān)鍵字時(shí),我們就稱這個(gè)函數(shù)為生成器函數(shù);當(dāng)我們調(diào)用這個(gè)生成器函數(shù)時(shí),Python會(huì)自動(dòng)在返回的對(duì)象中添加__iter__()方法和__next__()方法,它返回的對(duì)象就是一個(gè)生成器。
代碼示例:
>>> from collections.abc import Iterator >>> def generator(): print('first') yield 1 print('second') yield 2 print('third') yield 3 >>> gen = generator() >>> isinstance(gen, Iterator) True
2.2 next(生成器)
既然生成器是一種特殊的迭代器,那么我們對(duì)它使用一下next()方法:
>>> next(gen) first 1 >>> next(gen) second 2 >>> next(gen) third 3 >>> next(gen) Traceback (most recent call last): File "<pyshell#19>", line 1, in <module> next(gen) StopIteration
這里我想給這個(gè)generator()函數(shù)加一個(gè)return,最后會(huì)在拋出異常時(shí)打印這個(gè)返回值(這里我對(duì)Python異常相關(guān)的知識(shí)了解比較少,不太清楚這個(gè)問(wèn)題,以后再補(bǔ)充吧):
>>> from collections.abc import Iterator >>> def generator(): print('first') yield 1 print('second') yield 2 print('third') yield 3 return 'end' >>> gen = generator() >>> isinstance(gen, Iterator) True >>> next(gen) first 1 >>> next(gen) second 2 >>> next(gen) third 3 >>> next(gen) Traceback (most recent call last): File "<pyshell#7>", line 1, in <module> next(gen) StopIteration: end
可以看到,當(dāng)我們對(duì)生成器使用next()方法時(shí),生成器會(huì)執(zhí)行到下一個(gè)yield為止,并且返回yield后面的值;當(dāng)我們?cè)俅握{(diào)用next(生成器)時(shí),會(huì)繼續(xù)向下執(zhí)行,直到下一個(gè)yield語(yǔ)句;執(zhí)行到最后再?zèng)]有yield語(yǔ)句時(shí),就會(huì)拋出StopIteration異常
2.3 生成器和迭代器
通過(guò)上面的過(guò)程,我們知道了生成器本質(zhì)上就是一種迭代器,但是除了yield的特殊外,生成器還有什么特殊點(diǎn)呢——惰性計(jì)算。
這里的惰性計(jì)算是指:當(dāng)我們調(diào)用next(生成器)時(shí),每次調(diào)用只會(huì)產(chǎn)生一個(gè)值,這樣的好處就是,當(dāng)遍歷的元素量很大時(shí),我們不需要將所有的元素一次獲取,而是每次只取其中的一個(gè)元素,可以節(jié)省大量?jī)?nèi)存。(個(gè)人理解:這里注意和上面的迭代器的next()區(qū)別開(kāi),對(duì)于迭代器,雖然每次next()時(shí),也只會(huì)返回一個(gè)值,但是本質(zhì)上我們已經(jīng)把所有的值存儲(chǔ)在內(nèi)存中了(比如類(lèi)A和類(lèi)B的self.m_Lst),但是對(duì)于生成器,內(nèi)存中并不會(huì)將所有的值先存儲(chǔ)起來(lái),而是每次調(diào)用next()就獲取一個(gè)值)
下面我們來(lái)看一個(gè)實(shí)際的例子:輸出10000000以內(nèi)的所有偶數(shù)(注意,如果實(shí)際業(yè)務(wù)環(huán)境下需要存儲(chǔ),那就根據(jù)實(shí)際情況來(lái),這里只是針對(duì)兩者的區(qū)別進(jìn)行討論)
首先我們通過(guò)迭代器來(lái)實(shí)現(xiàn):(這里直接使用列表)
>>> def iterator(): lst = [] index = 0 while index <= 10000000: if index % 2 == 0: print(index) lst.append(index) index += 1 return lst >>> result = iterator()
然后通過(guò)生成器來(lái)實(shí)現(xiàn):
>>> def generator(): index = 0 while index <= 10000000: if index % 2 == 0: yield index index += 1 >>> gen = generator() >>> next(gen) 0 >>> next(gen) 2 >>> next(gen) 4 >>> next(gen) 6 >>> next(gen) 8
由于采取了惰性運(yùn)算,生成器也有它的不足:對(duì)于列表對(duì)象、字典對(duì)象等可迭代對(duì)象,我們可以通過(guò)len()方法直接獲取其長(zhǎng)度,但是對(duì)于生成器對(duì)象,我們只知道當(dāng)前元素,自然就不能獲取到它的長(zhǎng)度信息了。
下面我們總結(jié)一下生成器和迭代器的相同點(diǎn)和不同點(diǎn):
生成器是一種特殊的迭代器;迭代器會(huì)通過(guò)return來(lái)返回值,而生成器則是通過(guò)yield來(lái)返回值,對(duì)生成器使用next()方法,會(huì)在每一個(gè)yield語(yǔ)句處停下;迭代器會(huì)存儲(chǔ)所有的元素,但是生成器采用的是惰性計(jì)算,只知道當(dāng)前元素。
2.4 生成器解析式
列表解析式是我們常用的一種解析式:(類(lèi)似的還有字典解析式、集合解析式)
>>> lst = [i for i in range(10) if i % 2 == 1] >>> lst [1, 3, 5, 7, 9]
而生成器解析式和列表解析式類(lèi)似,我們只需要將[]更換為()即可:(把元組解析式給搶了,hh)
>>> gen = (i for i in range(10) if i % 2 == 1) >>> gen <generator object <genexpr> at 0x00000193E2945A80> >>> next(gen) 1 >>> next(gen) 3 >>> next(gen) 5 >>> next(gen) 7 >>> next(gen) 9 >>> next(gen) Traceback (most recent call last): File "<pyshell#11>", line 1, in <module> next(gen) StopIteration
至此,我們就有了生成器的兩種創(chuàng)造方式:
生成器函數(shù)(yield)返回一個(gè)生成器生成器解析式返回一個(gè)生成器 3 解決問(wèn)題
最后回到我們最初的問(wèn)題:如何在遍歷list或dict的時(shí)候正常刪除?
首先我們來(lái)探尋一下出錯(cuò)的原因,以list對(duì)象為例:
>>> lst = [1, 2, 3] >>> for i in lst: print(i) lst.remove(i) 1 3
可以看到,我們?cè)诒闅v打印列表元素的同時(shí)刪除當(dāng)前元素,實(shí)際的輸出和我們需要的輸出并不一樣。以下是個(gè)人理解(想更準(zhǔn)確地解答這個(gè)問(wèn)題可能需要進(jìn)一步結(jié)合源碼):
remove刪除列表元素時(shí),列表元素的索引會(huì)發(fā)生變化(這是因?yàn)镻ython底層列表是通過(guò)數(shù)組實(shí)現(xiàn)的,remove方法刪除元素時(shí)需要挪動(dòng)其他元素,具體分析我后續(xù)會(huì)補(bǔ)充相關(guān)源碼學(xué)習(xí)筆記,這里先了解即可)
類(lèi)比我們自定義實(shí)現(xiàn)的迭代器,可以看到我們會(huì)在__next__()方法中對(duì)索引進(jìn)行遞增:
>>> class A: def __init__(self, lst): self.m_Lst = lst def __iter__(self): print('call A().__iter__()') return B(self.m_Lst) >>> class B: def __init__(self, lst): self.m_Lst = lst self.m_Index= 0 def __iter__(self): print('call B().__iter__()') return self def __next__(self): print('call B().__next__()') try: value = self.m_Lst[self.m_Index] self.m_Index += 1 return value except IndexError: print('call B().__next__() except IndexError') raise StopIteration()
那么我們可以猜測(cè):列表對(duì)象對(duì)應(yīng)的迭代器,應(yīng)該也是會(huì)有一個(gè)索引成員變量,用于在__next__()方法中進(jìn)行定位(這里沒(méi)看過(guò)源碼,只是個(gè)人猜想)
當(dāng)我們使用for循環(huán)遍歷列表對(duì)象時(shí),實(shí)際上是通過(guò)next()方法對(duì)其對(duì)應(yīng)的迭代器進(jìn)行操作,此時(shí)由于remove()方法的調(diào)用,導(dǎo)致列表元素的索引發(fā)生了改變(原來(lái)元素3的索引是2,刪除元素2之后索引變?yōu)榱?),所以在__next__()方法中,此時(shí)需要遍歷的元素索引為1,而元素3頂替了這個(gè)位置,所以最后的輸出為1,3。
dict和list類(lèi)似,不過(guò)在遍歷時(shí)刪除dict中的元素時(shí)會(huì)直接報(bào)錯(cuò),具體原因大家也可以自行分析。
以上就是Python中for循環(huán)可迭代對(duì)象迭代器及生成器學(xué)習(xí)的詳細(xì)內(nèi)容,更多關(guān)于Python循環(huán)迭代生成器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
pandas dataframe中雙中括號(hào)和單中括號(hào)的區(qū)別及說(shuō)明
這篇文章主要介紹了pandas dataframe中雙中括號(hào)和單中括號(hào)的區(qū)別及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08pycharm: 恢復(fù)(reset) 誤刪文件的方法
今天小編就為大家分享一篇pycharm: 恢復(fù)(reset) 誤刪文件的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-10-10Django實(shí)現(xiàn)跨域請(qǐng)求過(guò)程詳解
這篇文章主要介紹了Django實(shí)現(xiàn)跨域請(qǐng)求過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-07-07在python中將list分段并保存為array類(lèi)型的方法
今天小編就為大家分享一篇在python中將list分段并保存為array類(lèi)型的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-07-07python實(shí)現(xiàn)手機(jī)通訊錄搜索功能
這篇文章主要介紹了python模仿手機(jī)通訊錄搜索功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02python pandas 時(shí)間日期的處理實(shí)現(xiàn)
這篇文章主要介紹了python pandas 時(shí)間日期的處理實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07