在閱讀Python tutorial類這一章的時(shí)候出現(xiàn)了iterator的概念,我是一個(gè)是編程的半吊子,雖然在其它語(yǔ)言(比如Java和C++)中也聽(tīng)過(guò)這個(gè)概念,但是一直沒(méi)認(rèn)真的去理解,這次我參考了一些文章,總結(jié)了一些我的看法。
首先,我在理解相關(guān)的概念的時(shí)候總是試圖探索引入相關(guān)概念的背后的真正意圖,我們看到的多半都是用法,那么為什么要這么用,也許搞清楚了每件事情背后的目的,接下來(lái)產(chǎn)生的解決方案才能順理成章水到渠成。那么這篇文章大多數(shù)是我通過(guò)現(xiàn)有的一些線索,推測(cè)出背后的一些可能的,也許我的理解充滿各種主觀因素,但是至少能夠自圓其說(shuō),也請(qǐng)各位能不吝賜教。其次,其中有些表述可能會(huì)有些啰嗦。
1. “迭代”這個(gè)概念引入主要是解決什么問(wèn)題的
首先你要知道什么叫做迭代
迭代就是單向地、逐個(gè)地訪問(wèn)某個(gè)容器中的元素的行為。 (可以理解為線性的方式訪問(wèn)容器中元素,單向、逐個(gè)是其特征)
在程序?qū)崿F(xiàn)中我們最常進(jìn)行的一種操作就是將容器(以下認(rèn)為“數(shù)據(jù)結(jié)構(gòu)”和“容器”是同義詞)里元素一個(gè)接一個(gè)的取出來(lái),但是為了實(shí)現(xiàn)這個(gè)簡(jiǎn)單的目的,針對(duì)不同的數(shù)據(jù)結(jié)構(gòu),我每次寫的代碼還都不一樣(這背后其實(shí)是要了解每個(gè)數(shù)據(jù)結(jié)構(gòu)的特性,并根據(jù)這些特性構(gòu)造合適的代碼),太麻煩了,于是我們想能不能這樣?能不能寫一個(gè)工具,每次我們需要在某個(gè)數(shù)據(jù)結(jié)構(gòu)上進(jìn)行迭代操作的時(shí)候,就調(diào)用這個(gè)工具,這個(gè)工具可以我們把不同數(shù)據(jù)結(jié)構(gòu)在方法實(shí)現(xiàn)細(xì)節(jié)上的不同屏蔽掉,這個(gè)工具就是迭代器,在python中是由類實(shí)現(xiàn)的。
迭代器是是一個(gè)抽象的概念,它代表了一種目的,而并非細(xì)節(jié)。所有實(shí)現(xiàn)了在某個(gè)特定數(shù)據(jù)結(jié)構(gòu)上進(jìn)行迭代行為的類都是迭代器。
延展概念
迭代是遍歷的一種特例,遍歷(traverse)是可以在數(shù)據(jù)結(jié)構(gòu)上來(lái)回的游走,不僅可以往前,還可以往后,同時(shí)還能保證不重不漏的,也就是把非線性的東西映射成線性的訪問(wèn)方式,而且還是不重不漏的,迭代是單向的而且只來(lái)一次。
2. 在這個(gè)程序語(yǔ)言中,這個(gè)概念是如何實(shí)現(xiàn)的
我們來(lái)看看這個(gè)東西在語(yǔ)言設(shè)計(jì)的層面是如何實(shí)現(xiàn)的:
我們假設(shè),在理想的情況下,如果有一個(gè)模塊或者一個(gè)函數(shù)里面實(shí)現(xiàn)了所有數(shù)據(jù)類型的迭代方式,假設(shè)這個(gè)模塊叫iterator,里面有一個(gè)方法叫做iteration,那么每次我用的時(shí)候,先import iterator,然后用iterator.iteration(帶迭代的變量)實(shí)現(xiàn)了一次迭代,這是比較理想的方式,但是現(xiàn)實(shí)中很難做到這點(diǎn),除了系統(tǒng)內(nèi)置的數(shù)據(jù)結(jié)構(gòu)之外,用戶自己實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)咋辦,于是放棄這種大一統(tǒng)的思路,而把所有的實(shí)現(xiàn)都下放給用戶自己實(shí)現(xiàn)。在這種情況下,為了保證每個(gè)用戶寫的迭代器能夠被其它用戶使用,需要制定一些規(guī)范讓大家遵守,我斗膽把這個(gè)規(guī)范稱之為“迭代規(guī)范”吧,這個(gè)規(guī)范可以分成兩個(gè)層面來(lái)理解,一個(gè)是使用層面,一個(gè)是實(shí)現(xiàn)層面。
迭代器協(xié)議的要點(diǎn)和難點(diǎn)在next方法的實(shí)現(xiàn)上(這為生成器的引入埋下了伏筆),而在next方法的實(shí)現(xiàn)有三個(gè)要點(diǎn),其中最難的一點(diǎn)在于理解“數(shù)據(jù)現(xiàn)用現(xiàn)生成”或者說(shuō)“用到某個(gè)元素的時(shí)候才把它生成(計(jì)算)出來(lái)”的思想。
這里就要引入另一個(gè)問(wèn)題了,我斗膽把它稱之為“數(shù)據(jù)的準(zhǔn)備問(wèn)題”。
啥意思呢?比方說(shuō)在程序中,在用一個(gè)數(shù)據(jù)之前我們首先得“有”一個(gè)數(shù)據(jù),那么我們?nèi)绾巍坝小币粋€(gè)數(shù)據(jù)?
兩種方法,一種方式是我把所有可能要用到元素都生成出來(lái)并且全部保存在內(nèi)存中然后再在使用的過(guò)程中去拿我要用的元素;還有一種是我用某個(gè)元素的前一刻時(shí)才去生成元素,然后再去用。你會(huì)覺(jué)得,我靠這還算一個(gè)問(wèn)題么,必須后一種啊,前一種明顯是在浪費(fèi)空間么(參考firstn的例子)。很不幸的時(shí)候,在python2有很多底層的數(shù)據(jù)實(shí)現(xiàn)就是沒(méi)有效率,你單看他們的時(shí)候都沒(méi)有問(wèn)題,但是一旦代碼的規(guī)模變大,層層調(diào)用之后就不好說(shuō)了。好,我們統(tǒng)一觀點(diǎn)之后,再討論下一個(gè)問(wèn)題。
在使用數(shù)據(jù)的前一刻把數(shù)據(jù)生成可能嗎?
其實(shí)是可能的,這也分兩種可能,一種可能是我們要自己“憑空創(chuàng)造”數(shù)據(jù),其實(shí)說(shuō)“憑空創(chuàng)造”其實(shí)不準(zhǔn)確,因?yàn)楸举|(zhì)上任何可列的數(shù)據(jù)都是自然數(shù)的函數(shù)(這個(gè)函數(shù)是數(shù)學(xué)意義上的函數(shù)),只要有函數(shù)(映射法則)以及自變量,求出因變量不是很簡(jiǎn)單的事情嗎?還有一種可能就是數(shù)據(jù)原來(lái)就存在了(比如說(shuō)存在數(shù)據(jù)庫(kù)里或者文件里),我們只需要取出來(lái)就可以,那么在這種情況下我們只要獲取保存數(shù)據(jù)的位置信息就能獲取到實(shí)際的數(shù)據(jù)值了,其實(shí)也可以理解為建立自然數(shù)和位置信息的映射。理解了這點(diǎn),另外兩個(gè)實(shí)現(xiàn)細(xì)節(jié)就好說(shuō)了,比如我們經(jīng)常會(huì)用(不是絕對(duì))一個(gè)游標(biāo)來(lái)記錄位置信息,可能這個(gè)游標(biāo)是一個(gè)計(jì)數(shù)器,然后我們利用循環(huán)的方式讓其自增從而實(shí)現(xiàn)迭代特性中的“單向”,“逐個(gè)”特征。再有就是最后一定要返回StopIteration。小結(jié)一下next方法的要點(diǎn)就是:
1. 數(shù)據(jù)現(xiàn)用現(xiàn)生成
2. 單向,逐個(gè)特性的實(shí)現(xiàn)
3. 最后返回StopIteration
在語(yǔ)法上,返回StopIteration比其它兩點(diǎn)都重要,next方法啥也不做,直接返回StopIteration都行。例如下面這個(gè)例子
>>> class Simplest_Iterator(object):
... def next(self): # Python 2 compatibility
... return self.__next__()
... def __next__(self):
... raise StopIteration
...
>>> z = Simplest_Iterator()
>>> next(z)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in __next__
StopIteration
值得注意的是,依據(jù)在“數(shù)據(jù)的可迭代聲明”這一步中iter方法返回的容器對(duì)象是自身還是其它對(duì)象的不同,迭代器在使用上會(huì)表現(xiàn)出不同的特性。
1. iter方法返回的是容器對(duì)象本身。這種實(shí)現(xiàn)方式叫做“容器本身既是迭代器”,這種情況迭代器構(gòu)造出來(lái)之后只能使用一次。
2. iter方法返回的是其他的對(duì)象。這種實(shí)現(xiàn)方式叫做“數(shù)據(jù)和迭代器的分離”,其實(shí)這個(gè)更像是工具的思想,我第一次閱讀與迭代器實(shí)現(xiàn)相關(guān)的文檔時(shí)腦海里首先想到是這種方法。
那么這兩種方式有什么區(qū)別呢?在Python中,大多數(shù)容器類型(哪些屬于容器類型,參考這篇文章)都是采用第二種方式實(shí)現(xiàn)的,都是采用“數(shù)據(jù)和迭代器分離”的實(shí)現(xiàn)的方式的,這是因?yàn)椤翱傻逼鋵?shí)從觀念上意味著,我在這個(gè)種容器上反復(fù)進(jìn)行迭代的,如果“容器本身即是迭代器”的話,每次容器每次調(diào)用iter方法返回的是自身,在經(jīng)過(guò)一次迭代之后,游標(biāo)指到容器最后一個(gè)元素后面,同時(shí)拋出了異常,迭代器就再也無(wú)法工作了,除非你利用一些方法把next方法中的游標(biāo)置為初始狀態(tài)。而我們采用數(shù)據(jù)和迭代器分離的方式話,每次容器調(diào)用iter方法的時(shí)候,都會(huì)重新生成一個(gè)新的迭代器對(duì)象,這樣就可以無(wú)限次的在容器上進(jìn)行迭代了。
下面用例子來(lái)說(shuō)明,我們實(shí)現(xiàn)一個(gè)與內(nèi)置函數(shù)xrange的類似的類
實(shí)現(xiàn)方式1:“容器本身即是迭代器”
class yrange:
def __init__(self, n):
self.i = 0
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.i < self.n:
i = self.i
self.i += 1
return i
else:
raise StopIteration()
之后我們來(lái)做一下測(cè)試:
>>> y = yrange(3)
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 14, in next
StopIteration
實(shí)現(xiàn)方式2. 數(shù)據(jù)和迭代器的分離的例子:
class zrange:
def __init__(self, n):
self.n = n
def __iter__(self):
return zrange_iter(self.n)
class zrange_iter:
def __init__(self, n):
self.i = 0
self.n = n
def __iter__(self):
# Iterators are iterables too.
# Adding this functions to make them so.
return self
def __next__(self):
if self.i < self.n:
i = self.i
self.i += 1
return i
else:
raise StopIteration()
然后我們比較一下兩個(gè)類的不同
>>> y = yrange(5)
>>> list(y)
[0, 1, 2, 3, 4]
>>> list(y)
[]
>>> z = zrange(5)
>>> list(z)
[0, 1, 2, 3, 4]
>>> list(z)
[0, 1, 2, 3, 4]
3. 這些概念有什么用?
毫無(wú)疑問(wèn)第一個(gè)應(yīng)用就是做遍歷
學(xué)過(guò)數(shù)據(jù)結(jié)構(gòu)的都知道所有操作的基礎(chǔ)都是基于遍歷,如果解決了遍歷的問(wèn)題,就等于說(shuō)解決了一大半的問(wèn)題
甚至文件都可以進(jìn)行迭代哦
可以用來(lái)生成數(shù)據(jù)(我估計(jì)生成器的名字就是這樣來(lái)的吧)
在迭代器實(shí)現(xiàn)的核心思想就是“數(shù)據(jù)用到的時(shí)候才生成”,那么我們是不是可以用這個(gè)方式來(lái)生成數(shù)據(jù)?而且這種實(shí)現(xiàn)方式意外的節(jié)省空間?這點(diǎn)正好引入下一個(gè)要討論的知識(shí)點(diǎn),也就是生成器
最有說(shuō)服力的是那個(gè)斐波那契數(shù)列的例子
4. 生成器
簡(jiǎn)單來(lái)說(shuō),生成器就是迭代器實(shí)現(xiàn)的簡(jiǎn)化。相比用一個(gè)實(shí)現(xiàn)迭代器協(xié)議的類來(lái)實(shí)現(xiàn)迭代器而言,我們只要定義一個(gè)函數(shù)就可以實(shí)現(xiàn)迭代器,這個(gè)函數(shù)就是生成器。
那么這個(gè)函數(shù)如何定義呢?我們以yrange迭代器為例,生成器實(shí)現(xiàn)如下:
def yrange(n):
i = 0
while i < n:
yield i
i += 1
怎么理解這個(gè)代碼,有很多人說(shuō)這個(gè)東西簡(jiǎn)單,但是我不這么覺(jué)得,其實(shí)越是看似簡(jiǎn)單的東西里面越是有深刻的知識(shí)在里面。就像我在next方法實(shí)現(xiàn)那部分中對(duì)“在使用數(shù)據(jù)的前一刻把數(shù)據(jù)生成可能嗎?”這個(gè)問(wèn)題進(jìn)行的討論一樣,對(duì)于數(shù)據(jù)的生成,除了在已經(jīng)存儲(chǔ)數(shù)據(jù)的地方進(jìn)行獲取以外,還有一種方式是“把它計(jì)算出來(lái)”,而計(jì)算的方法,本質(zhì)上是建立一個(gè)從自然數(shù)序列到我們所要的序列的一個(gè)映射。對(duì)于后一種方式來(lái)說(shuō),我們要做的事情就是,遍歷自然數(shù)序列,對(duì)于每個(gè)自然數(shù),把它作為映射法則的因變量放入到映射法則中去進(jìn)行運(yùn)算,把運(yùn)算出來(lái)的結(jié)果返回出去。比如映射關(guān)系是f(x),那么這個(gè)代碼可以這么寫:
def generatorexample(n):
i = 0
while i < n:
yield f(i)
i += 1
也就說(shuō)把計(jì)算的結(jié)果(在程序中就是一個(gè)表達(dá)式)返回出去的那步用yield關(guān)鍵字來(lái)返回就行了。
那么這個(gè)背后實(shí)現(xiàn)機(jī)制是怎么樣的,
1. 當(dāng)generator function被調(diào)用的時(shí)候,這個(gè)函數(shù)會(huì)返回一個(gè)generator對(duì)象之后什么都不做。
2. 當(dāng)next方法被調(diào)用的時(shí)候,函數(shù)就會(huì)開(kāi)始執(zhí)行直到y(tǒng)ield所在的位置,計(jì)算出來(lái)的值在這個(gè)位置被返回,之后這個(gè)函數(shù)就停下了。之后再調(diào)用next方法的時(shí)候,函數(shù)繼續(xù)執(zhí)行,直到遇到下一個(gè)yield。
3. 如果執(zhí)行完的代碼,還沒(méi)有遇到y(tǒng)ield,就會(huì)拋出StopIteration異常。
>>> def foo():
... print "begin"
... for i in range(3):
... print "before yield", i
... yield i
... print "after yield", i
... print "end"
...
>>> f = foo()
>>> f.next()
begin
before yield 0
0
>>> f.next()
after yield 0
before yield 1
1
>>> f.next()
after yield 1
before yield 2
2
>>> f.next()
after yield 2
end
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
再比如:
def my_generator():
print("return first value")
yield 1
print("return second value")
yield 2
print("return last value")
yield 3
print("raise StopIteration")
z = my_generator()
next(z)
next(z)
next(z)
next(z)
這里面有一些地方一下說(shuō)明一下,其實(shí)如果深究起“generator”來(lái),其實(shí)這還是一個(gè)非常模糊的詞呢,generator到底是說(shuō)那個(gè)函數(shù)呢?還是說(shuō)這個(gè)函數(shù)返回的值呢?
在官方的glossary里面,generator就指得是那個(gè)包含yield語(yǔ)句的函數(shù)體,也就是“generator function”,而這個(gè)如果要指代這個(gè)函數(shù)返回的對(duì)象,一般稱之為“generator iterator”,官方建議為了避免歧義,最好把詞說(shuō)全一點(diǎn)。
而我還是喜歡這篇文章的描述,“generator function ”就是那個(gè)函數(shù)體,“generator”表示“generator function”這個(gè)函數(shù)返回的對(duì)象。
5. 生成器表達(dá)式
生成器表達(dá)式可以看成迭代器在生成器的基礎(chǔ)上進(jìn)一步簡(jiǎn)化(還能在懶一點(diǎn)嗎),用好理解的話說(shuō)就是——生成器表達(dá)式可以看成列表推導(dǎo)式的生成器版。列表推導(dǎo)是可以看我的另一篇文章
雖所簡(jiǎn)化了形式,但是我感覺(jué)更接近迭代器的本質(zhì)了——也就是“構(gòu)造一個(gè)和自然數(shù)序列一一對(duì)應(yīng)的序列”
6. 總結(jié)一下這幾個(gè)概念的關(guān)系
借用一張圖
7.迭代思想在Python中的廣泛存在
在tutorial里面有這么一句話The use of iterators pervades and unifies Python.
基本上來(lái)說(shuō)迭代的思想在Python這門語(yǔ)言的實(shí)現(xiàn)過(guò)程中已經(jīng)滲透在各個(gè)角落,已經(jīng)是底層的設(shè)計(jì)思想了,很多語(yǔ)法都是基于迭代這個(gè)概念向上建造的。以下是一些例子
很多容器類型都是iterable
甚至文件類型都是可以用for語(yǔ)句來(lái)訪問(wèn)的。我們最常用的一個(gè)數(shù)據(jù)結(jié)構(gòu)list,它是用iterable作為參數(shù)來(lái)初始化一個(gè)list,其實(shí)執(zhí)行了這樣的初始化函數(shù)
class list(object):
...
def __init__(self, iterable):
for i in iterable:
self.value.append(i)
...
很多函數(shù)的參數(shù)以及返回值都是iterable
map(), filter() ,zip() ,range()
dict.keys(), dict.items() 和 dict.values()
for其實(shí)也是語(yǔ)法糖
基于迭代的語(yǔ)法糖
比如:
for i in iterable:
func(i)
本質(zhì)上是:
z = iter(iterable)
try:
while True:
func(next(z))
except StopIteration:
pass
unpack也是語(yǔ)法糖
比如:
>>> a,b = b,a # 等價(jià)于 a,b = (b,a)
>>> a,b,*_ = [1,2.3,4] # 僅適用于 Python 3
>>> a,b,*_ = iter([1,2.3,4]) # 也可以用于迭代器
>>> a
1
>>> b
2.3
>>> _
[4]
其實(shí)是賦值是這么實(shí)現(xiàn)的:
k = iter(iterable)
a = next(k)
b = next(k)
_ = list(k)
list comperhension也是語(yǔ)法糖
上面雖然說(shuō)generator experssion是生成器版本的list comperhension,這只是為了便于理解,其實(shí)先后順序應(yīng)該顛倒過(guò)來(lái)。
List Comprehension 也只是語(yǔ)法糖而已,甚至還可以寫出 tuple/set/dict comprehension(其實(shí) set 就是所有 key 的 value 都為 None 的 dict)
>>> [x*x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> list(x*x for x in range(10))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> tuple(x*x for x in range(10))
(0, 1, 4, 9, 16, 25, 36, 49, 64, 81)
>>> set(x*x for x in range(10))
{0, 1, 64, 4, 36, 9, 16, 49, 81, 25}
>>> dict((x,x*x) for x in range(10))
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
參考文獻(xiàn)
- Python Practice Book 我的不少例子都引自這個(gè)
- 官網(wǎng)wiki對(duì)于iterator的解釋 官方的迭代器的解釋 Q&A 不錯(cuò) 說(shuō)明了iter方法和next方法的不一定非要是一個(gè) 鏈接里的IBM鏈接失效了,但是搜索一下就行了
- 官網(wǎng)wiki對(duì)于generator的解釋 節(jié)省空間的例子不錯(cuò)
- 官方文檔對(duì)于iterator protocol的解釋
- Python 筆記(3):可迭代變量 后面的好多語(yǔ)法糖的例子都來(lái)自這篇文章,感謝這位作者,PS:你的博客里的那個(gè)背景音也是我喜歡的,很想和你交個(gè)朋友呢,可是你沒(méi)寫聯(lián)系方式
- python迭代器與生成器小結(jié) 這篇文章值得一看
- Iterables vs. Iterators vs. Generators 引用率也很高的文章,我引用了其中一張圖片,根據(jù)我的理解修改了一些圖片
- Python yield 使用淺析斐波那契數(shù)列的例子 仔細(xì)看了一下這個(gè)居然是廖雪峰寫的
|