Python——五分鐘帶你弄懂迭代器與生成器,夯實代碼能力

本文始發於我的公衆號:TechFlow,原創不易,求個關注node


今天是週一Python專題,給你們帶來的是Python當中生成器和迭代器的使用。python

我當初第一次學到迭代器和生成器的時候,並無太在乎,只是以爲這是一種新的獲取數據的方法。對於獲取數據的方法而言,咱們會一種就足夠了。可是在我後來Python的使用以及TensorFlow等學習使用當中,我發現不少地方都用到了迭代器和生成器,或者是直接使用,或者是借鑑了思路。因此咱們不能掉以輕心,今天就讓咱們仔細來看看,它們究竟是怎麼回事。web

迭代器

咱們先從迭代器[1]開始入手,迭代器並非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。和咱們剛纔說的同樣,咱們每一次調用,它會自動日後移動一格,獲取後面一位的數據。flex

這裏有一點須要注意,由於咱們建立的數組當中一共只有5個元素,若是咱們調用it的次數超過5次,那麼會引起超界,Python的解釋器會拋出StopIteration的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 * 3 for i in range(10)]
複製代碼

也就是說咱們把循環放在list的定義當中,這樣Python會自動執行裏面的循環,而後將全部循環的結果進行二次計算後寫入到list當中去。咱們稍微變形一下,就獲得了一個最簡單的生成器。

g = (i * 3 for 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
while True:
if n < 3:
yield n
n += 1
else:
yield 10


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():
yield from 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 is not None:
yield from self.lchild.iterate()
yield self.key
if self.rchild is not None:
yield from 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):
yield from self.root.iterate()
複製代碼

經過yield from,咱們能夠很輕鬆地利用遞歸的思路來實現樹上的生成器。從而能夠很方便地以生成器的思路來遍歷樹上全部的元素。

到這裏,關於Python當中迭代器和生成器的知識就算是講完了,這二者的概念有些接近,可是又不徹底同樣,不少初學者容易搞混淆。

其實能夠這麼理解,迭代器和生成器遍歷元素的方式是同樣的,都是經過調用next來獲取下一個元素。咱們經過yield建立函數,返回的結果其實就是生成器生成的數據的迭代器。也就是說迭代器只是迭代和獲取數據的,可是並不能無中生有地創造數據。而生成器的主要做用是創造數據,它生成出來的數據是以迭代器的形式返回的。

舉個例子,你開了一個奶茶店,經過奶茶店每月能夠在銀行帳戶裏得到一筆收入。迭代器就是這個帳戶,經過它你能夠得到一筆一筆的收入。而奶茶店則是一個生成器,它產出數據,可是是以迭代器的形式返回給你的,也就是以銀行帳戶的方式給你收入。咱們拿到銀行卡並不知道它裏面的錢是怎麼賺來的,只能看到錢,也就是說咱們並不知道迭代器背後數據的邏輯。可是生成器咱們是清楚的,由於錢(生產邏輯)是咱們親自賺來的。

今天的文章就是這些,若是以爲有所收穫,請順手點個關注或者轉發吧,大家的舉手之勞對我來講很重要。

參考資料

[1]

programiz: "https://www.programiz.com/python-programming/iterator"

[2]

廖雪峯的Python教程: "https://www.liaoxuefeng.com/wiki/1016959663602400/1017323698112640"

相關文章
相關標籤/搜索