前言
迭代器和生成器可能對(duì)于一些人來說知道是什么東東,但是并沒有比較深入的了解,那么今天,就跟隨我來了解一下這兩者的概念,關(guān)系及優(yōu)點(diǎn)。我將使用python中的迭代器和生成器作為演示,如果你不懂python沒關(guān)系,明白了概念,剩下的就只是編程語言的差異了!這一點(diǎn)很關(guān)鍵,再啰嗦一句,不要為了編程而編程,也要明白一些概念性的東西,編程語言只是工具!
從循環(huán)開始說起
想必大家在學(xué)習(xí)編程的時(shí)候,肯定學(xué)到過for循環(huán),while循環(huán),do...while循環(huán)等等,那么我們?yōu)槭裁葱枰h(huán)操作呢?因?yàn)橛行r(shí)候我們希望計(jì)算機(jī)為我們重復(fù)的執(zhí)行同樣的操作,比如我有一個(gè)“數(shù)組”,里面存儲(chǔ)了100個(gè)同學(xué)的id,那么我則會(huì)對(duì)這個(gè)數(shù)組進(jìn)行循環(huán)操作,然后挨個(gè)輸出。當(dāng)然還有很多其他地方需要循環(huán)操作,這里我只是舉個(gè)例子。
所以,循環(huán)操作是計(jì)算機(jī)編程語言中必不可少的組成部分,那么請(qǐng)大家用幾秒鐘時(shí)間回想一下,我們之前曾經(jīng)寫過的循環(huán)操作for循環(huán),while循環(huán)。我們往往需要初始化一個(gè)變量i,還得聲明一個(gè)條件比如i<100,然后循環(huán)完每一步之后做什么,比如(下方偽代碼):
for(i = 0; i < 100; i++) {
}
我們可以很容易的用這種循環(huán)來遍歷一個(gè)數(shù)組,希望大家學(xué)過數(shù)據(jù)結(jié)構(gòu),因?yàn)閿?shù)組在內(nèi)存中的存儲(chǔ)是連續(xù)的!我們可以通過數(shù)組的“下標(biāo)”(其實(shí)是相對(duì)于數(shù)組第一個(gè)元素的位置)來進(jìn)行訪問數(shù)組中的元素,所以在很多時(shí)候,我們通過for循環(huán)來遍歷數(shù)組(下方偽代碼):
for(i = 0; i < arrLength; i++) {
}
那么如果我現(xiàn)在問你,你怎么進(jìn)行遍歷一個(gè)沒有在內(nèi)存中連續(xù)存儲(chǔ)的“數(shù)據(jù)結(jié)構(gòu)”呢,比如python中的“字典”,javascript中的”對(duì)象“,又比如你自己寫了一個(gè)”樹“結(jié)構(gòu)的類,想遍歷整個(gè)樹的節(jié)點(diǎn)?那么傳統(tǒng)的for循環(huán),while循環(huán)就無法發(fā)揮他們的作用了,這個(gè)時(shí)候我們就應(yīng)該引入”迭代器“了。
所以,”迭代器“其實(shí)目的也是為了”循環(huán)“,更嚴(yán)謹(jǐn)一些,是為了“遍歷”,你可以把迭代器看成比普通循環(huán)更高級(jí)別的工具,普通循環(huán)能搞定的迭代器也能搞定,普通循環(huán)搞不定的迭代器還能搞定,并且使用迭代器比普通循環(huán)效率更高,這個(gè)我們后面說到生成器的時(shí)候會(huì)提到。
迭代(iteration)/可迭代(iterable)/迭代器(iterator)
我想大多數(shù)人可能和我一樣,剛開始對(duì)這些概念/名詞都很模糊,那么讓我們一起弄明白他們。
大家先要知道“協(xié)議”(protocol)的意思,其實(shí)協(xié)議是用來“規(guī)范/標(biāo)準(zhǔn)化”你“創(chuàng)造的東西”的。比如,你開天辟地的發(fā)明了一種東西叫做“吧啦嗶哩”,你給小明說:“小明,給我發(fā)一個(gè)吧啦嗶哩過來”,如果小明不知道啥叫“吧啦嗶哩”,那么小明會(huì)直接懵逼的。這時(shí)候你就要定一個(gè)“協(xié)議”如下:
- "吧啦嗶哩"一共有10個(gè)字
- "吧啦嗶哩"開頭和結(jié)尾都是"#"號(hào) (占兩個(gè)字)
- "吧啦嗶哩"最后四位是"blbl"
- 其他隨便
那么我們根據(jù)這個(gè)協(xié)議,可以很輕易的構(gòu)造出“吧啦嗶哩”來:#1234blbl# 或者 #8888blbl#
同樣,我們根據(jù)這份協(xié)議,就可以用來檢測(cè)你得到的是不是“吧啦嗶哩”,#1234blbl# -> 是,#1234blbl!-> 不是
迭代(iteration)
明白了上面的東西,下面我們就開始“迭代”之旅,迭代顧名思義,就是重復(fù)的的既定的任務(wù),直到完成。所以,為了完成迭代,我們需要一個(gè)迭代器!那么什么是迭代器呢?來看看迭代器的協(xié)議吧
迭代器協(xié)議 iterator protocol
從前有個(gè)人發(fā)明了迭代器,為了讓大家明白什么是迭代器,他就寫了這個(gè)協(xié)議,那么協(xié)議的內(nèi)容簡(jiǎn)而言之就是一句話:如果一個(gè)對(duì)象包括一個(gè)叫"next"(python3 為__next__)的方法,那么這個(gè)對(duì)象就叫做“迭代器”。
好了,那么我們根據(jù)這個(gè)協(xié)議可以創(chuàng)建一個(gè)迭代器(iterator)
class Counter:
def __init__(self):
self.index = 0
def __next__(self):
i = self.index
if i < 10:
self.index += 1
return i
這個(gè)Counter就是一個(gè)迭代器,但是目前它沒有什么太大的作用,因?yàn)槲覀儾豢赡苊看瓮ㄟ^手動(dòng)調(diào)用__next__方法來進(jìn)行操作。
好消息是,很多編程軟件為我們提供了一個(gè)“語法糖”(syntactic sugar),讓這個(gè)語法糖來替我們反復(fù)執(zhí)行__next__方法,比如python中的"for.. in",但是,為了讓這個(gè)反復(fù)執(zhí)行的過程停下來,我們同樣需要定義一個(gè)終止信號(hào),在python中,終止信號(hào)就是拋出一個(gè)StopIteration的“例外”(exception),來告知我們的語法糖:”好啦,沒東西可以迭代了,可以停了“,這樣迭代就終止了。
所以我們?cè)龠M(jìn)一步規(guī)范一下我們創(chuàng)建的迭代器成如下形式:
class Counter:
def __init__(self):
self.index = 0
def __next__(self):
i = self.index
if i < 10:
self.index += 1
return i
else:
raise StopIteration
好了,我們來試一下:
counter = Counter()
for i in counter:
print(i)
不妙,報(bào)錯(cuò)了。。
TypeError: 'Counter' object is not iterable
錯(cuò)誤顯示說:這個(gè)Counter對(duì)象不是可迭代的!這是什么意思呢?
原來,為了使用這個(gè)for..in 迭代語法糖,我們需要在in后面放可以迭代的“迭代器”,什么是可以迭代?你可以認(rèn)為就是可以使用for..in語法糖,讓語法糖幫你重復(fù)調(diào)用next方法就好了。如果不可以迭代,那么for..in這個(gè)語法糖就無法為我們自動(dòng)調(diào)用next方法。
所以說,為了使用for..in語法糖來進(jìn)行迭代我們的迭代器,你必須讓你的迭代器可迭代(有點(diǎn)繞。。哈哈)。
這句話有兩層含義:
- 為了使用for..in語法糖,你必須讓你的迭代器可迭代
- 你如果不適用for..in語法糖,你就不必讓你的迭代器可迭代,你可以自己寫一個(gè)語法糖,不斷地調(diào)用next方法,當(dāng)遇到StopIteration例外的時(shí)候停止罷了。
好了,我們現(xiàn)在明白了,通常來講,當(dāng)我們要?jiǎng)?chuàng)建了一個(gè)迭代器時(shí),我們還“必須”(注意是必須)讓迭代器可迭代,這樣理解:因?yàn)橐粋€(gè)不可迭代的迭代器是沒有意義的!
所以,注意!從現(xiàn)在開始到文章結(jié)束,我所說的“迭代器”都是“可迭代”的迭代器!
那么怎么讓我的迭代器可迭代呢?同樣,來看什么是“可迭代協(xié)議”(iterable protocol)
可迭代協(xié)議 iterable protocol
在python中,為了使一個(gè)”對(duì)象“可迭代:
- 這個(gè)迭代器必須同時(shí)包含另一個(gè)方法叫做“__iter__”
- 這個(gè)"__iter__"方法還得返回一個(gè)”迭代器“(可迭代)
請(qǐng)注意,上面我說的是:為了使一個(gè)”對(duì)象“可迭代,這里,對(duì)象可以指我們剛剛創(chuàng)建的”Counter“迭代器,也可以是其他的對(duì)象。
來個(gè)栗子:
為了使我們剛才創(chuàng)建的Counter迭代器對(duì)象“可迭代”,那么:
- 我們就在這個(gè)Counter對(duì)象里面添加一個(gè)叫__iter__的方法 (可迭代化操作)
- 讓這個(gè)__iter__方法返回一個(gè)“可迭代的迭代器” (這里就是自己了!)
class Counter:
def __init__(self):
self.index = 0
def __iter__(self):
return self
def __next__(self):
i = self.index
if i < 10:
self.index += 1
return i
else:
raise StopIteration
counter = Counter()
for i in counter:
print(i)
Cool! 這個(gè)時(shí)候我們得到了0,1,2,3,4,5,6,7,8,9的迭代!
這里簡(jiǎn)單說一些執(zhí)行步驟,當(dāng)我們使用for..in語法糖的時(shí)候,它先調(diào)用__iter__方法,得到返回的迭代器,然后連續(xù)調(diào)用該迭代器的__next__方法,知道遇到StopIteration例外。
我上面也提到了,我們不僅可以使迭代器“可迭代”,我們也可以使普通的對(duì)象“可迭代”,只需給該對(duì)象添加一個(gè)__iter__的方法,然后返回一個(gè)可迭代的迭代器就好了!
這里順便插一句!在python中,我們可以使用"iter"這個(gè)函數(shù)來返回一個(gè)“可迭代的迭代器”。
比如:
x = iter([1, 2, 3])
print(x) #<list_iterator object at 0x10c828550>
x.__next__() # 返回 1
x.__next__() # 返回 2
x.__next__() # 返回 3
x.__next__() # 返回 StopIteration
所以,我們可以讓一個(gè)普通對(duì)象可迭代,而不一定非得是迭代器。
class Name:
def __iter__(self):
return iter(['zhangsan', 'lisi', 'wangwu'])
name = Name()
for n in name:
print(n)
不錯(cuò)!我們得到了zhangsan, lisi, wangwu
現(xiàn)在邏輯不是很復(fù)雜的情況之下,這種創(chuàng)建迭代器的方式還是能夠接受的,但是如果邏輯復(fù)雜,以及用這種模式多了,每次這么定義就不是很方便,于是為了“簡(jiǎn)化”創(chuàng)建迭代器的過程,“生成器”generator就出現(xiàn)了。
生成器generator
生成器的出現(xiàn),就是為了簡(jiǎn)化創(chuàng)建迭代器的繁雜,同時(shí)又要保證邏輯的清晰,說到底生成器就是為了更方便我們使用迭代器而生的,生成器的特性如下:
- 生成器的樣子就是一個(gè)普通的函數(shù),只不過return關(guān)鍵詞被yield取代了
- 當(dāng)調(diào)用這個(gè)“函數(shù)”的時(shí)候,它會(huì)立即返回一個(gè)迭代器,而不立即執(zhí)行函數(shù)內(nèi)容,直到調(diào)用其返回迭代器的next方法是才開始執(zhí)行,直到遇到y(tǒng)ield語句暫停。
- 繼續(xù)調(diào)用生成器返回的迭代器的next方法,恢復(fù)函數(shù)執(zhí)行,直到再次遇到y(tǒng)ield語句
- 如此反復(fù),一直到遇到StopIteration
看如下例子:
def gFun():
print('before hello')
yield 'hello'
print('after hello')
a = gFun() # 調(diào)用生成器函數(shù),返回一個(gè)迭代器并賦給a
print(a) # <generator object gFun at 0x104cd2a40> 得到一個(gè)生成器對(duì)象(迭代器)
print(a.__next__())
# before hello
# hello
print(a.__next__())
# after hello
# StopIteration
同時(shí)因?yàn)檎{(diào)用生成器函數(shù)返回的是一個(gè)迭代器,所以我們可以使用for..in語法糖對(duì)其進(jìn)行迭代操作:
a = gFun()
for x in a:
print(x)
迭代返回了before hello, hello, after hello
使用迭代器/生成器的好處
首先快速看一段代碼:
def firstn(n):
num, nums = 0, []
while num < n:
nums.append(num)
num += 1
return nums
sum_of_first_n = sum(firstn(1000000))
這段代碼定一個(gè)了一個(gè)函數(shù)firstn,該函數(shù)接受一個(gè)參數(shù)n,返回n之前所有的整數(shù),最后對(duì)這些整數(shù)進(jìn)行求和。
這個(gè)代碼使用了我們傳統(tǒng)的while循環(huán),如果接受的參數(shù)n比較小還好,但是當(dāng)接受的參數(shù)很大時(shí),對(duì)內(nèi)存的消耗就凸顯出來了,因?yàn)樵趫?zhí)行該函數(shù)的過程中,nums這個(gè)大的列表會(huì)全部存在于內(nèi)存中。并且求和運(yùn)算只有當(dāng)nums列表完全構(gòu)建完成之后才可以進(jìn)行運(yùn)算,效率也高。
而用迭代器(生成器)的方法則會(huì)大大提高效率,一方面每次next循環(huán)都會(huì)yield出一個(gè)值,供sum函數(shù)累加使用,這樣就不用占用很大的內(nèi)存,另一方面,使用迭代器/生成器也不用完全等到前n個(gè)數(shù)全部遍歷完再進(jìn)行累加,效率更高!