您好,登錄后才能下訂單哦!
python中迭代器與生成器的區別?針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
迭代器
我們先從迭代器開始入手,迭代器并不是Python獨有的概念,在C++和Java當中都有iterator的概念,兩者的使用也都差不多。迭代器主要解決了一個問題,在一個復雜場景下,獲取數據怎么盡可能簡便。
我們來假設一個場景,假設我們從某個數據源獲取了一批數據。然后我們需要調用前一萬條生成一個結果,得到結果之后,我們要將剩下的數據交給另一個調用方去處理。這個過程看起來非常平常,但是隱藏了兩個問題,第一個問題是如果我們能保證第一次處理的時候,每次都是使用一萬條還好說,如果我們使用的條數是一個動態的值呢?顯然,我們需要一個變量來記錄我們究竟用了多少條數據,和這批數據的狀態。其次,如果這個數據量很大會存在一個數據傳輸的問題。我們每次都要將一大批數據傳來傳去,顯然會消耗很多資源。
還有一個場景是如果我們開發的是一個比較復雜的數據結構,比如一棵多叉樹,下游想要遍歷它的時候,必須要了解它的實現原理才行。這顯然也不太友好。
迭代器的出現正是針對以上這些問題,它的含義也很簡單,有點像是我們遍歷鏈表的時候用到的cur的指針。永遠指向當前的位置,永遠知道下一個位置在哪里。
容器迭代器
我們先從簡單的元素迭代器開始了解它的用途,我們都知道Python當中經典的幾個容器:list
, tuple
和dict
。它們都是一個可迭代對象,我們可以直接使用關鍵字iter獲取一個對應的迭代器。
我們來看一個例子:
arr = [1, 3, 4, 5, 9] it = iter(arr) print(next(it)) print(next(it))
這是一個非常經典的例子,我們首先定義了一個數組,然后通過iter
關鍵字獲取了一個讀取它的迭代器。有了迭代器之后我們可以通過next
關鍵字獲取迭代器當中的下一個元素,我們一共調用了兩次next
,第一次輸出的結果是1,第二次的結果是3。和我們剛才說的一樣,我們每一次調用,它會自動往后移動一格,獲取后面一位的數據。
這里有一點需要注意,因為我們創建的數組當中一共只有5個元素,如果我們調用it的次數超過5次,那么會引發超界,Python的解釋器會拋出StopIterat****ion的error。
除了使用next,我們也可以使用for循環來迭代它:
for i in it: print(i)
這種用法就和我們用for循環遍歷元素是一樣的。
自定義迭代器
官方的迭代器的用法就這么多,這也不是它的主要用法,它最主要的用法是我們自己創建迭代器。和之前介紹Python自定義排序的時候的思路一樣,我們為類添加上__iter__
方法和__next__
方法即可。
其中__iter__
方法用來初始化并返回迭代器,關于它的解釋比較復雜。在Python當中迭代有兩個概念一個是iterable
,一個是iterator
。協議規定iteratble的__iter__方法會返回一個iterator。而iterator本身也是一個iterable對象,自然也需要實現__iter__方法。
我知道這么說可能聽不太明白,我舉個例子,比如說員工和老板,員工沒有審批權限,只能轉達給老板。我們把員工比喻成iterable對象,老板比喻成iterator
。
員工面臨一個問題的時候沒有權限處理,只能找來老板決定。也就是最終決定的是老板,但如果是老板自己發現的問題,他完全可以自己就解決了,不需要再去找其他人。所以說我們用iter調用iterable對象的__iter__
的時候,會得到一個iterator,也就是調用員工返回老板,然后通過調用iterator的__next__
來進行迭代。
到這里也就清楚了,只有iterator有__next__
方法,而iterable沒有,并且__iter__返回的是一個iterator。然而我們定義的已經是iterator了,它同時也是一個iterable對象,所以調用__iter__時只需要返回self
就好了。__next__方法很簡單,對應迭代器的next方法,用來返回下一個迭代的元素。
我們來看一個例子:
class PowTwo: """Class to implement an iterator of powers of two""" def __init__(self, max = 0): self.max = max def __iter__(self): self.n = 0 return self def __next__(self): if self.n <= self.max: result = 2 ** self.n self.n += 1 return result else: raise StopIteration
這是一個簡單的生成2的冪的迭代器,我們在__iter__
里為self.n
初始化為0,然后返回自身。在__next__里判斷有沒有迭代結束,如果結束的話拋出一個異常。
我們來看使用它的例子:
>>> a = PowTwo(4) >>> i = iter(a) >>> next(i) 1 >>> next(i) 2 >>> next(i) 4 >>> next(i) 8 >>> next(i) 16 >>> next(i) Traceback (most recent call last): ... StopIteration
我們也可以用for循環來迭代它:
>>> for i in PowTwo(5): ... print(i) ... 1 2 4 8 16 32
迭代器除了可以迭代一個容器或者是像上面這樣自定義迭代方法之外,還可以用來迭代生成器。下面就讓我們一起來看下生成器的概念。
生成器
生成器的概念和迭代器相輔相成,迭代器是生成一個遍歷數據的迭代工具,而生成器則是數據生成工具。
舉個很簡單的例子,比如說斐波那契數列我們都知道,從第三個數開始等于前面兩個數的和。比如我們想獲取100萬個斐波那契數列,按照傳統的方法我們需要開辟一個長度是一百萬的數組,然后按照斐波那契數列的定義一個一個地計算。顯然這樣會消耗大量的空間,有沒有辦法我們和迭代器那樣構建一個生成數據的方法,我們每次調用獲取下一個結果呢?這樣我們要多少數據就調用多少次就可以了,從根本上解決了存儲的問題。
下面我們來看怎么定義一個生成器。
括號創建法
最簡單的方法真的很簡單,和我們創建list基本上一模一樣。
在Python當中,我們經常這樣初始化一個數組:
arr = [i * 3for i in range(10)]
也就是說我們把循環放在list的定義當中,這樣Python會自動執行里面的循環,然后將所有循環的結果進行二次計算后寫入到list當中去。我們稍微變形一下,就得到了一個最簡單的生成器。
g = (i * 3for i in range(10)) print(next(g))
看清楚了嗎,其實和list沒什么差別,只是我們將最外層的括號從[]換成了()。
這種方法大家應該都能看懂,但是可能會有一個疑惑。我們這樣做的意義是什么呢?這樣和上面用[]定義有什么區別呢?
其實是有區別的,如果沒有區別,那么我們用生成器也就沒有意義了。它的區別也就是生成器的意義,簡單來說,我們前文中已經說過了當定義一個list的時候,Python會自動將for循環執行一遍,然后將結果寫入進list當中。但是生成器不會,雖然我們也用到了for循環,但是它只是起到了限制個數的作用,在執行完這一步之后,Python并不會將for循環執行結束。只有我們每次調用next,才會觸發它進行一次循環。
不相信的同學可以試試,看看運行一下下面兩個語句的區別:
g = (i for i in range(1000000000)) g = [i for i in range(1000000000)]
如果奇怪的事情發生了,不妨再回到文章來思考一下。
函數創建法
上面介紹的方法雖然簡單,但是不太實用,因為很多時候我們想要的數據構造方法會比較復雜,很難用這種形式展現出來。
所以Python當中還為我們提供了一種構造生成器的方法,相比起來要稍微復雜一點點,但是也很好用。我們來看一個例子:
def gtr(n): for i in range(n): yield i
從代碼上來看,我們好像定義了一個函數,某種程度上可以這么理解,但是它返回的結果并不是一個值,而是一個生成器[2]。
如果你真的去試了,你會得到一個generator類型的實例,這也是Python自帶的生成器的實例。
再仔細觀察一下,你會發現這個函數當中的關鍵字和一般的不太一樣,它沒有使用return,而是使用了yield。yield和return在很大程度上很接近,但是又有些不同。
相同點是當我們執行到yield時,和return一樣會將yield之后的內容返回給調用方。比如上面代碼當中寫到yield i,那么我們運行next的時候就會獲取到這個i。
不同的地方是,當我們下一次再次執行的時候,會繼續從上次yield處開始往下執行。有些類似于遞歸的時候,底層的遞歸執行結束回到上層的情況。因此如果我們要獲取多個值,需要在生成器當中使用循環。舉個例子:
def test(): n = 0 whileTrue: if n < 3: yield n n += 1 else: yield10 if __name__ == '__main__': t = test() for i in range(10): print(next(t))
我們如果執行上面這段代碼,前三個數是0,1和2,從第四個數開始一直是10。如果你能看懂這個例子,一定能明白yield的含義。
yield from
接下來要介紹的yield from和yield用法差不多,也是從生成器返回一個結果,并且下次執行的時候從返回的位置開始繼續執行。
但是它有一點和yield不同,我們來看一個經典的例子。
def g1(): yield range(5) def g2(): yieldfrom range(5) it1 = g1() it2 = g2() for x in it1: print(x) for x in it2: print(x)
這兩者打印出來的結果是一樣的,但是邏輯完全不同。在第一個生成器g1當中,直接通過yield返回了一個迭代器。也就是說我們for循環執行的其實是range(5),而第二個生成器g2則通過yield from獲取了range(5)這個迭代器當中的值進行的返回。
也就是說yield from可以返回一個迭代器或者是生成器執行next之后的結果。
最后,我們來看一個yield from使用的一個經典場景:二叉樹的遍歷:
class Node: def __init__(self, key): self.key = key self.lchild = None self.rchild = None self.iterated = False self.father = None def iterate(self): if self.lchild isnotNone: yieldfrom self.lchild.iterate() yield self.key if self.rchild isnotNone: yieldfrom self.rchild.iterate()
在這個代碼當中我們定義了二叉樹當中的一個節點,以及它對應的迭代方法。由于我們用到了yield來返回結果,所以iterate方法本質是一個生成器。再來看iterate方法內部,我們通過yield from調用了iterate,所以我們在執行的時候,它會自動繼續解析node.lchild的iterate,也就是說我們通過yield from實現了遞歸。
當我們建好樹之后,可以直接使用root.iterate來遍歷整棵樹。
class Tree: def __init__(self): #建樹過程 self.root = Node(4) self.root.lchild = Node(3) self.root.lchild.father = self.root self.root.rchild = Node(5) self.root.rchild.father = self.root self.root.lchild.lchild = Node(1) self.root.lchild.lchild.father = self.root.lchild self.root.rchild.rchild = Node(7) self.root.rchild.rchild.father = self.root.rchild def iterate(self): yieldfrom self.root.iterate()
通過yield from
,我們可以很輕松地利用遞歸的思路來實現樹上的生成器。從而可以很方便地以生成器的思路來遍歷樹上所有的元素。
到這里,關于Python當中迭代器和生成器的知識就算是講完了,這兩者的概念有些接近,但是又不完全一樣,很多初學者容易搞混淆。
其實可以這么理解,迭代器和生成器遍歷元素的方式是一樣的,都是通過調用next來獲取下一個元素。我們通過yield創建函數,返回的結果其實就是生成器生成的數據的迭代器。也就是說迭代器只是迭代和獲取數據的,但是并不能無中生有地創造數據。而生成器的主要作用是創造數據,它生成出來的數據是以迭代器的形式返回的。
舉個例子,你開了一個奶茶店,通過奶茶店每個月可以在銀行賬戶里獲得一筆收入。迭代器就是這個賬戶,通過它你可以獲得一筆一筆的收入。而奶茶店則是一個生成器,它產出數據,但是是以迭代器的形式返回給你的,也就是以銀行賬戶的方式給你收入。我們拿到銀行卡并不知道它里面的錢是怎么賺來的,只能看到錢,也就是說我們并不知道迭代器背后數據的邏輯。但是生成器我們是清楚的,因為錢(生產邏輯)是我們親自賺來的。
關于python中迭代器與生成器的區別問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。