<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
之前在學習list和dict相關的知識時,遇到了一個常見的問題:如何在遍歷list或dict的時候正常刪除?例如我們在遍歷dict的時候刪除,會報錯:RuntimeError: dictionary changed size during iteration;而在遍歷list的時候刪除,會有部分元素刪除不完全。
由這個問題又引發了我對另一個問題的思考:我們通過for迴圈去遍歷一個list或dict時,具體是如何for的呢?即for迴圈的本質是什麼?
在查閱了相關資料後,我認識到這是一個和迭代器相關的問題,所以藉此機會來詳細認識一下Python中的for迴圈、可迭代物件、迭代器和生成器
“迭代是重複反饋過程的活動,其目的通常是為了逼近所需目標或結果。”在Python中,可迭代物件、迭代器、for迴圈都是和“迭代”密切相關的知識點。
在Python中,稱可以迭代的物件為可迭代物件。要判斷一個類是否可迭代,只需要判斷這個類是否為Iterable類的範例即可:
>>> from collections.abc import Iterable >>> isinstance([], Iterable) True >>> isinstance(123, Iterable) False
上述提供了一個判斷物件是否為可迭代物件的方法,那麼一個物件怎麼才是可迭代物件呢——只需要該物件的類實現了__iter__()方法即可:
>>> class A: pass >>> isinstance(A(), Iterable) False >>> class B: def __iter__(self): pass >>> isinstance(B(), Iterable) True
由此可見,只要一個類實現了__iter__()方法,那麼這個類的範例物件就是可迭代物件。注意這裡的__iter__()方法可以沒有任何內容。
在Python中,通過Iterator類與迭代器相對應。相較於可迭代物件,迭代器只是多實現了一個__next__()方法:
>>> from collections.abc import Iterator >>> class C: def __iter__(self): pass def __next__(self): pass >>> isinstance(C(), Iterator) True
顯然,迭代器一定是可迭代物件(因為迭代器同時實現了__iter__()方法和__next__()方法),而可迭代物件不一定是迭代器。
我們來看一下內建型別中的可迭代物件是否為迭代器:
>>> isinstance(C(), Iterator) True >>> isinstance([], Iterable) True >>> isinstance([], Iterator) False >>> isinstance('123', Iterable) True >>> isinstance('123', Iterator) False >>> isinstance({}, Iterable) True >>> isinstance({}, Iterator) False
由此可見,str、list、dict物件都是可迭代物件,但它們都不是迭代器。
至此,我們對可迭代物件和迭代器有了一個基本概念上的認識,也知道了有__iter__()和__next__()這兩種方法。但是這兩個魔法方法究竟是如何使用的呢?它們和for迴圈又有什麼關係呢?
iter()方法和next()方法都是Python提供的內建方法。對物件使用iter()方法會呼叫物件的__iter__()方法,對物件使用next()方法會呼叫物件的__next__()方法。下面我們具體看一下它們之間的關係。
__iter__()方法的作用就是返回一個迭代器,一般我們可以通過內建函數iter()來呼叫物件的__iter__()方法
1.2中舉的例子,只是簡單的實現了__iter__()方法,但函數體直接被pass掉了,本質上是沒有實現迭代功能的,現在我們來看一下__iter__()正常使用時的例子:
>>> class A: def __iter__(self): print('執行A類的__iter__()方法') return B() >>> class B: def __iter__(self): print('執行B類的__iter__()方法') return self def __next__(self): pass >>> a = A() >>> a1 = iter(a) 執行A類的__iter__()方法 >>> b = B() >>> b1 = iter(b) 執行B類的__iter__()方法
可以看到,對於類A,我們為它的__iter__()方法設定了返回值為B(),而B()就是一個迭代器;而對於類B,我們在它的__iter__()方法中直接返回了它的範例self,因為它的範例本身就是可迭代物件。當然這裡我們也可以返回其他的迭代器,但是如果__iter__()方法返回的是一個非迭代器,那麼當我們呼叫iter()方法時就會報錯:
>>> 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'
__next__()方法的作用是返回遍歷過程中的下一個元素,如果沒有下一個元素,則會丟擲StopIteration異常,一般我們可以通過內建函數next()來呼叫物件的__next__()方法
下面我們以list物件為例,來看一下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
可以看到,當我們直接對列表物件l1使用next()方法時,會報錯’list’ object is not an iterator,顯然list物件並不是迭代器,也就是說它沒有實現__next__()方法,那麼我們怎麼才能去”對一個列表物件使用next()“呢——根據我們前面介紹的__iter__()方法,我們知道它會返回一個迭代器,而迭代器是實現了__next__()方法的,所以我們可以先對list物件使用iter__(),獲取到它對應的迭代器,然後對這個迭代器使用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__()為什麼要不停地去取出元素,並且在最後去丟擲異常,而不是通過物件的長度相關資訊來確定呼叫次數?
個人認為是因為我們可以通過next()去手動呼叫物件的__next__()方法,而在next()中並沒有判斷物件的長度,所以需要在__next__()去處理
下面我們試著通過實現自定義一下list的迭代過程:
首先我們定義一個類A,它是一個可迭代物件,__iter__()方法會返回一個迭代器B(),並且還擁有一個成員變數m_Lst:
>>> class A: def __init__(self, lst): self.m_Lst = lst def __iter__(self): return B(self.m_Lst)
對於迭代器的類B,我們實現它的__iter__()方法和__next__()方法,注意在__next__()方法中我們需要丟擲StopIteration異常。此外,它擁有兩個成員變數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()
至此,我們已經完成了迭代器的準備工作,下面我們來實踐一下迭代吧,為了更好地展示這個過程,我們可以加上一些列印:
>>> 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()方法能夠很好地將整個遍歷的過程展示出來。至此,我們對可迭代物件、迭代器以及__iter__()和__next__()都有了一定的認識,那麼,for迴圈和它們有什麼關係呢?
for迴圈是我們使用頻率最高的操作之一,我們一般會用它來遍歷一個容器(列表、字典等),這些容器都有一個共同的特點——都是可迭代物件。那麼對於我們自定義的類A,它的範例物件a應該也可以通過for迴圈來遍歷:
>>> 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
通過列印,我們可以清楚的看到:對一個可迭代物件使用for迴圈進行遍歷時,for迴圈會呼叫該物件的__iter__()方法來獲取到迭代器,然後迴圈呼叫該迭代器的__next__()方法,依次獲取下一個元素,並且最後會捕獲StopIteration異常(這裡可以嘗試在類B的__next__()方法最後只捕獲IndexError而不丟擲StopIteration,則for迴圈此時會無限迴圈)
既然我們提到了for迴圈會自動去捕獲StopIteration異常,當沒有捕獲到StopIteration異常時會無限迴圈,那麼我們是否可以用while迴圈來模擬一下這個過程呢?
>>> 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
到這裡,大家應該對for對可迭代物件遍歷的過程有了一定的瞭解,想要更深入瞭解的話可以結合原始碼進一步學習(本次學習分享主要是結合實際程式碼對一些概念進行講解,並未涉及到相應原始碼)。
迭代器和生成器總是會被同時提起,那麼它們之間有什麼關聯呢——生成器是一種特殊的迭代器。
當一個函數體內使用yield關鍵字時,我們就稱這個函數為生成器函數;當我們呼叫這個生成器函數時,Python會自動在返回的物件中新增__iter__()方法和__next__()方法,它返回的物件就是一個生成器。
程式碼範例:
>>> 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
既然生成器是一種特殊的迭代器,那麼我們對它使用一下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
這裡我想給這個generator()函數加一個return,最後會在丟擲異常時列印這個返回值(這裡我對Python異常相關的知識瞭解比較少,不太清楚這個問題,以後再補充吧):
>>> 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
可以看到,當我們對生成器使用next()方法時,生成器會執行到下一個yield為止,並且返回yield後面的值;當我們再次呼叫next(生成器)時,會繼續向下執行,直到下一個yield語句;執行到最後再沒有yield語句時,就會丟擲StopIteration異常
通過上面的過程,我們知道了生成器本質上就是一種迭代器,但是除了yield的特殊外,生成器還有什麼特殊點呢——惰性計算。
這裡的惰性計算是指:當我們呼叫next(生成器)時,每次呼叫只會產生一個值,這樣的好處就是,當遍歷的元素量很大時,我們不需要將所有的元素一次獲取,而是每次只取其中的一個元素,可以節省大量記憶體。(個人理解:這裡注意和上面的迭代器的next()區別開,對於迭代器,雖然每次next()時,也只會返回一個值,但是本質上我們已經把所有的值儲存在記憶體中了(比如類A和類B的self.m_Lst),但是對於生成器,記憶體中並不會將所有的值先儲存起來,而是每次呼叫next()就獲取一個值)
下面我們來看一個實際的例子:輸出10000000以內的所有偶數(注意,如果實際業務環境下需要儲存,那就根據實際情況來,這裡只是針對兩者的區別進行討論)
首先我們通過迭代器來實現:(這裡直接使用列表)
>>> def iterator(): lst = [] index = 0 while index <= 10000000: if index % 2 == 0: print(index) lst.append(index) index += 1 return lst >>> result = iterator()
然後通過生成器來實現:
>>> 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
由於採取了惰性運算,生成器也有它的不足:對於列表物件、字典物件等可迭代物件,我們可以通過len()方法直接獲取其長度,但是對於生成器物件,我們只知道當前元素,自然就不能獲取到它的長度資訊了。
下面我們總結一下生成器和迭代器的相同點和不同點:
生成器是一種特殊的迭代器;迭代器會通過return來返回值,而生成器則是通過yield來返回值,對生成器使用next()方法,會在每一個yield語句處停下;迭代器會儲存所有的元素,但是生成器採用的是惰性計算,只知道當前元素。
列表解析式是我們常用的一種解析式:(類似的還有字典解析式、集合解析式)
>>> lst = [i for i in range(10) if i % 2 == 1] >>> lst [1, 3, 5, 7, 9]
而生成器解析式和列表解析式類似,我們只需要將[]更換為()即可:(把元組解析式給搶了,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
至此,我們就有了生成器的兩種創造方式:
生成器函數(yield)返回一個生成器生成器解析式返回一個生成器 3 解決問題
最後回到我們最初的問題:如何在遍歷list或dict的時候正常刪除?
首先我們來探尋一下出錯的原因,以list物件為例:
>>> lst = [1, 2, 3] >>> for i in lst: print(i) lst.remove(i) 1 3
可以看到,我們在遍歷列印列表元素的同時刪除當前元素,實際的輸出和我們需要的輸出並不一樣。以下是個人理解(想更準確地解答這個問題可能需要進一步結合原始碼):
remove刪除列表元素時,列表元素的索引會發生變化(這是因為Python底層列表是通過陣列實現的,remove方法刪除元素時需要挪動其他元素,具體分析我後續會補充相關原始碼學習筆記,這裡先了解即可)
類比我們自定義實現的迭代器,可以看到我們會在__next__()方法中對索引進行遞增:
>>> 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()
那麼我們可以猜測:列表物件對應的迭代器,應該也是會有一個索引成員變數,用於在__next__()方法中進行定位(這裡沒看過原始碼,只是個人猜想)
當我們使用for迴圈遍歷列表物件時,實際上是通過next()方法對其對應的迭代器進行操作,此時由於remove()方法的呼叫,導致列表元素的索引發生了改變(原來元素3的索引是2,刪除元素2之後索引變為了1),所以在__next__()方法中,此時需要遍歷的元素索引為1,而元素3頂替了這個位置,所以最後的輸出為1,3。
dict和list類似,不過在遍歷時刪除dict中的元素時會直接報錯,具體原因大家也可以自行分析。
以上就是Python中for迴圈可迭代物件迭代器及生成器學習的詳細內容,更多關於Python迴圈迭代生成器的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45