前言
迭代器和生成器可能對于一些人來說知道是什么東東旅挤,但是并沒有比較深入的了解切威,那么今天伍宦,就跟隨我來了解一下這兩者的概念砚殿,關系及優(yōu)點啃憎。我將使用python中的迭代器和生成器作為演示,如果你不懂python沒關系似炎,明白了概念辛萍,剩下的就只是編程語言的差異了悯姊!這一點很關鍵,再啰嗦一句贩毕,不要為了編程而編程悯许,也要明白一些概念性的東西,編程語言只是工具辉阶!
從循環(huán)開始說起
想必大家在學習編程的時候先壕,肯定學到過for循環(huán),while循環(huán)谆甜,do...while循環(huán)等等垃僚,那么我們?yōu)槭裁葱枰h(huán)操作呢?因為有些時候我們希望計算機為我們重復的執(zhí)行同樣的操作规辱,比如我有一個“數組”谆棺,里面存儲了100個同學的id,那么我則會對這個數組進行循環(huán)操作罕袋,然后挨個輸出改淑。當然還有很多其他地方需要循環(huán)操作,這里我只是舉個例子浴讯。
所以朵夏,循環(huán)操作是計算機編程語言中必不可少的組成部分,那么請大家用幾秒鐘時間回想一下兰珍,我們之前曾經寫過的循環(huán)操作for循環(huán)侍郭,while循環(huán)。我們往往需要初始化一個變量i掠河,還得聲明一個條件比如i<100亮元,然后循環(huán)完每一步之后做什么,比如(下方偽代碼):
for(i = 0; i < 100; i++) {
}
我們可以很容易的用這種循環(huán)來遍歷一個數組唠摹,希望大家學過數據結構爆捞,因為數組在內存中的存儲是連續(xù)的!我們可以通過數組的“下標”(其實是相對于數組第一個元素的位置)來進行訪問數組中的元素勾拉,所以在很多時候煮甥,我們通過for循環(huán)來遍歷數組(下方偽代碼):
for(i = 0; i < arrLength; i++) {
}
那么如果我現(xiàn)在問你,你怎么進行遍歷一個沒有在內存中連續(xù)存儲的“數據結構”呢藕赞,比如python中的“字典”成肘,javascript中的”對象“,又比如你自己寫了一個”樹“結構的類斧蜕,想遍歷整個樹的節(jié)點双霍?那么傳統(tǒng)的for循環(huán),while循環(huán)就無法發(fā)揮他們的作用了,這個時候我們就應該引入”迭代器“了洒闸。
所以染坯,”迭代器“其實目的也是為了”循環(huán)“,更嚴謹一些丘逸,是為了“遍歷”单鹿,你可以把迭代器看成比普通循環(huán)更高級別的工具,普通循環(huán)能搞定的迭代器也能搞定深纲,普通循環(huán)搞不定的迭代器還能搞定仲锄,并且使用迭代器比普通循環(huán)效率更高,這個我們后面說到生成器的時候會提到囤萤。
迭代(iteration)/可迭代(iterable)/迭代器(iterator)
我想大多數人可能和我一樣昼窗,剛開始對這些概念/名詞都很模糊是趴,那么讓我們一起弄明白他們涛舍。
大家先要知道“協(xié)議”(protocol)的意思,其實協(xié)議是用來“規(guī)范/標準化”你“創(chuàng)造的東西”的唆途。比如富雅,你開天辟地的發(fā)明了一種東西叫做“吧啦嗶哩”,你給小明說:“小明肛搬,給我發(fā)一個吧啦嗶哩過來”没佑,如果小明不知道啥叫“吧啦嗶哩”,那么小明會直接懵逼的温赔。這時候你就要定一個“協(xié)議”如下:
- "吧啦嗶哩"一共有10個字
- "吧啦嗶哩"開頭和結尾都是"#"號 (占兩個字)
- "吧啦嗶哩"最后四位是"blbl"
- 其他隨便
那么我們根據這個協(xié)議蛤奢,可以很輕易的構造出“吧啦嗶哩”來:#1234blbl# 或者 #8888blbl#
同樣,我們根據這份協(xié)議陶贼,就可以用來檢測你得到的是不是“吧啦嗶哩”啤贩,#1234blbl# -> 是,#1234blbl拜秧!-> 不是
迭代(iteration)
明白了上面的東西痹屹,下面我們就開始“迭代”之旅,迭代顧名思義枉氮,就是重復的的既定的任務志衍,直到完成。所以聊替,為了完成迭代楼肪,我們需要一個迭代器!那么什么是迭代器呢惹悄?來看看迭代器的協(xié)議吧
迭代器協(xié)議 iterator protocol
從前有個人發(fā)明了迭代器春叫,為了讓大家明白什么是迭代器,他就寫了這個協(xié)議,那么協(xié)議的內容簡而言之就是一句話:如果一個對象包括一個叫"next"(python3 為__next__)的方法象缀,那么這個對象就叫做“迭代器”蔬将。
好了,那么我們根據這個協(xié)議可以創(chuàng)建一個迭代器(iterator)
class Counter:
def __init__(self):
self.index = 0
def __next__(self):
i = self.index
if i < 10:
self.index += 1
return i
這個Counter就是一個迭代器央星,但是目前它沒有什么太大的作用霞怀,因為我們不可能每次通過手動調用__next__方法來進行操作。
好消息是莉给,很多編程軟件為我們提供了一個“語法糖”(syntactic sugar)毙石,讓這個語法糖來替我們反復執(zhí)行__next__方法,比如python中的"for.. in"颓遏,但是徐矩,為了讓這個反復執(zhí)行的過程停下來,我們同樣需要定義一個終止信號叁幢,在python中滤灯,終止信號就是拋出一個StopIteration的“例外”(exception),來告知我們的語法糖:”好啦曼玩,沒東西可以迭代了鳞骤,可以停了“,這樣迭代就終止了黍判。
所以我們再進一步規(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)
不妙,報錯了顷帖。美旧。
TypeError: 'Counter' object is not iterable
錯誤顯示說:這個Counter對象不是可迭代的!這是什么意思呢贬墩?
原來榴嗅,為了使用這個for..in 迭代語法糖,我們需要在in后面放可以迭代的“迭代器”震糖,什么是可以迭代录肯?你可以認為就是可以使用for..in語法糖,讓語法糖幫你重復調用next方法就好了吊说。如果不可以迭代论咏,那么for..in這個語法糖就無法為我們自動調用next方法。
所以說颁井,為了使用for..in語法糖來進行迭代我們的迭代器厅贪,你必須讓你的迭代器可迭代(有點繞。雅宾。哈哈)养涮。
這句話有兩層含義:
- 為了使用for..in語法糖,你必須讓你的迭代器可迭代
- 你如果不適用for..in語法糖,你就不必讓你的迭代器可迭代贯吓,你可以自己寫一個語法糖懈凹,不斷地調用next方法,當遇到StopIteration例外的時候停止罷了悄谐。
好了介评,我們現(xiàn)在明白了,通常來講爬舰,當我們要創(chuàng)建了一個迭代器時们陆,我們還“必須”(注意是必須)讓迭代器可迭代,這樣理解:因為一個不可迭代的迭代器是沒有意義的情屹!
所以坪仇,注意!從現(xiàn)在開始到文章結束垃你,我所說的“迭代器”都是“可迭代”的迭代器椅文!
那么怎么讓我的迭代器可迭代呢?同樣蜡镶,來看什么是“可迭代協(xié)議”(iterable protocol)
可迭代協(xié)議 iterable protocol
在python中雾袱,為了使一個”對象“可迭代:
- 這個迭代器必須同時包含另一個方法叫做“__iter__”
- 這個"__iter__"方法還得返回一個”迭代器“(可迭代)
請注意恤筛,上面我說的是:為了使一個”對象“可迭代官还,這里,對象可以指我們剛剛創(chuàng)建的”Counter“迭代器毒坛,也可以是其他的對象望伦。
來個栗子:
為了使我們剛才創(chuàng)建的Counter迭代器對象“可迭代”,那么:
- 我們就在這個Counter對象里面添加一個叫__iter__的方法 (可迭代化操作)
- 讓這個__iter__方法返回一個“可迭代的迭代器” (這里就是自己了煎殷!)
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! 這個時候我們得到了0屯伞,1,2豪直,3劣摇,4,5弓乙,6末融,7,8暇韧,9的迭代勾习!
這里簡單說一些執(zhí)行步驟,當我們使用for..in語法糖的時候懈玻,它先調用__iter__方法巧婶,得到返回的迭代器,然后連續(xù)調用該迭代器的__next__方法,知道遇到StopIteration例外艺栈。
我上面也提到了英岭,我們不僅可以使迭代器“可迭代”,我們也可以使普通的對象“可迭代”湿右,只需給該對象添加一個__iter__的方法巴席,然后返回一個可迭代的迭代器就好了!
這里順便插一句诅需!在python中漾唉,我們可以使用"iter"這個函數來返回一個“可迭代的迭代器”。
比如:
x = iter([1, 2, 3])
print(x) #<list_iterator object at 0x10c828550>
x.__next__() # 返回 1
x.__next__() # 返回 2
x.__next__() # 返回 3
x.__next__() # 返回 StopIteration
所以堰塌,我們可以讓一個普通對象可迭代赵刑,而不一定非得是迭代器。
class Name:
def __iter__(self):
return iter(['zhangsan', 'lisi', 'wangwu'])
name = Name()
for n in name:
print(n)
不錯场刑!我們得到了zhangsan, lisi, wangwu
現(xiàn)在邏輯不是很復雜的情況之下般此,這種創(chuàng)建迭代器的方式還是能夠接受的,但是如果邏輯復雜牵现,以及用這種模式多了铐懊,每次這么定義就不是很方便,于是為了“簡化”創(chuàng)建迭代器的過程瞎疼,“生成器”generator就出現(xiàn)了科乎。
生成器generator
生成器的出現(xiàn),就是為了簡化創(chuàng)建迭代器的繁雜贼急,同時又要保證邏輯的清晰茅茂,說到底生成器就是為了更方便我們使用迭代器而生的,生成器的特性如下:
- 生成器的樣子就是一個普通的函數太抓,只不過return關鍵詞被yield取代了
- 當調用這個“函數”的時候空闲,它會立即返回一個迭代器,而不立即執(zhí)行函數內容走敌,直到調用其返回迭代器的next方法是才開始執(zhí)行碴倾,直到遇到y(tǒng)ield語句暫停。
- 繼續(xù)調用生成器返回的迭代器的next方法掉丽,恢復函數執(zhí)行跌榔,直到再次遇到y(tǒng)ield語句
- 如此反復,一直到遇到StopIteration
看如下例子:
def gFun():
print('before hello')
yield 'hello'
print('after hello')
a = gFun() # 調用生成器函數机打,返回一個迭代器并賦給a
print(a) # <generator object gFun at 0x104cd2a40> 得到一個生成器對象(迭代器)
print(a.__next__())
# before hello
# hello
print(a.__next__())
# after hello
# StopIteration
同時因為調用生成器函數返回的是一個迭代器矫户,所以我們可以使用for..in語法糖對其進行迭代操作:
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))
這段代碼定一個了一個函數firstn,該函數接受一個參數n残邀,返回n之前所有的整數皆辽,最后對這些整數進行求和柑蛇。
這個代碼使用了我們傳統(tǒng)的while循環(huán),如果接受的參數n比較小還好驱闷,但是當接受的參數很大時耻台,對內存的消耗就凸顯出來了,因為在執(zhí)行該函數的過程中空另,nums這個大的列表會全部存在于內存中盆耽。并且求和運算只有當nums列表完全構建完成之后才可以進行運算,效率也高扼菠。
而用迭代器(生成器)的方法則會大大提高效率摄杂,一方面每次next循環(huán)都會yield出一個值,供sum函數累加使用循榆,這樣就不用占用很大的內存析恢,另一方面,使用迭代器/生成器也不用完全等到前n個數全部遍歷完再進行累加秧饮,效率更高映挂!