原文章地址:http://python.jobbole.com/87805/
學(xué)習(xí)這篇文章之前需要了解——迭代的概念
對于迭代這個(gè)詞,百度百科是這么翻譯的——重復(fù)反饋過程的活動臭蚁,其目的通常是為了逼近所需目標(biāo)或結(jié)果最铁。每一次對過程的重復(fù)稱為一次迭代,每一次迭代得到的結(jié)果會作為下一次迭代初始值
學(xué)習(xí)這篇文章之前需要了解——python assert斷言語句垮兑,點(diǎn)我跳轉(zhuǎn)
讓我們先看一張圖冷尉,這張圖解釋了他們之間的關(guān)系
容器 (container)
容器是一種把多個(gè)元素組織在一起的數(shù)據(jù)結(jié)構(gòu),容器中的元素可以逐個(gè)地迭代獲取系枪,可以用in, not in關(guān)鍵字判斷元素是否包含在容器中雀哨。通常這類數(shù)據(jù)結(jié)構(gòu)把所有的元素存儲在內(nèi)存中(也有一些特例,并不是所有的元素都放在內(nèi)存私爷,比如迭代器和生成器對象)在Python中雾棺,常見的容器對象有:
- list, deque, ….
- set, frozensets, ….
- dict, defaultdict, OrderedDict, Counter, ….
- tuple, namedtuple, …
- str
容器比較容易理解,因?yàn)槟憔涂梢园阉醋魇且粋€(gè)盒子衬浑、一棟房子捌浩、一個(gè)柜子,里面可以塞任何東西工秩。從技術(shù)角度來說尸饺,當(dāng)它可以用來詢問某個(gè)元素是否包含在其中時(shí)宏榕,那么這個(gè)對象就可以認(rèn)為是一個(gè)容器,比如 list侵佃,set麻昼,tuples都是容器對象:
詢問某元素是否在dict中用dict的中key:
詢問某substring是否在string中:
盡管絕大多數(shù)容器都提供了某種方式來獲取其中的每一個(gè)元素,但這并不是容器本身提供的能力馋辈,而是可迭代對象賦予了容器這種能力抚芦,當(dāng)然并不是所有的容器都是可迭代的,比如:Bloom filter迈螟,雖然Bloom filter可以用來檢測某個(gè)元素是否包含在容器中叉抡,但是并不能從容器中獲取其中的每一個(gè)值,因?yàn)锽loom filter壓根就沒把元素存儲在容器中答毫,而是通過一個(gè)散列函數(shù)映射成一個(gè)值保存在數(shù)組中褥民。
可迭代對象(iterable)
剛才說過,很多容器都是可迭代對象洗搂,此外還有更多的對象同樣也是可迭代對象消返,比如處于打開狀態(tài)的files,sockets等等耘拇。但凡是可以返回一個(gè)迭代器的對象都可稱之為可迭代對象(可迭代對象可以通過iter()方法返回一個(gè)迭代器(iterator))撵颊。也可以簡單的理解為可以直接作用于for循環(huán)的對象統(tǒng)稱為可迭代對象(Iterable)。聽起來可能有點(diǎn)困惑惫叛,沒關(guān)系倡勇,先看一個(gè)例子:
x = [1,2,3]
y = iter(x)
z = iter(x)
next(y)
Out[23]: 1
next(y)
Out[24]: 2
type(x)
Out[25]: list
type(y)
Out[26]: listiterator
這里x是一個(gè)可迭代對象,可迭代對象和容器一樣是一種通俗的叫法嘉涌,并不是指某種具體的數(shù)據(jù)類型妻熊,list是可迭代對象,dict是可迭代對象仑最,set也是可迭代對象扔役。y和z是兩個(gè)獨(dú)立的迭代器,迭代器內(nèi)部持有一個(gè)狀態(tài)词身,該狀態(tài)用于記錄當(dāng)前迭代所在的位置厅目,以方便下次迭代的時(shí)候獲取正確的元素番枚。迭代器有一種具體的迭代器類型法严,比如list_iterator,set_iterator葫笼∩钇。可迭代對象實(shí)現(xiàn)了iter方法,該方法返回一個(gè)迭代器對象路星。
當(dāng)運(yùn)行代碼:
x = [1,2,3]
for i in x:
...
實(shí)際執(zhí)行情況是:
迭代器(iterator)
那么什么迭代器呢溯街?它是一個(gè)帶狀態(tài)的對象诱桂,他能在你調(diào)用next()方法的時(shí)候返回容器中的下一個(gè)值,任何實(shí)現(xiàn)了iter和next()(python2中實(shí)現(xiàn)next())方法的對象都是迭代器呈昔,iter返回迭代器自身挥等,next返回容器中的下一個(gè)值,如果容器中沒有更多元素了堤尾,則拋出StopIteration異常肝劲。
所以,迭代器就是實(shí)現(xiàn)了工廠模式的對象郭宝,它在你每次你詢問要下一個(gè)值的時(shí)候給你返回辞槐。有很多關(guān)于迭代器的例子,比如itertools函數(shù)返回的都是迭代器對象粘室。
我們自定義一個(gè)迭代器
# python2.7
class test:
def __init__(self):
self.x = 0
def __iter__(self):
return self
def next(self):
value = self.x + 1
self.x = value
if self.x > 2:
raise StopIteration
return value
t = test()
a = iter(t)
next(a)
Out[47]: 1
next(a)
Out[67]: 2
next(a)
Traceback (most recent call last):
File "<ipython-input-82-3f6e2eea332d>", line 1, in <module>
next(a)
StopIteration
test既是一個(gè)可迭代對象(因?yàn)樗鼘?shí)現(xiàn)了iter方法)榄檬,又是一個(gè)迭代器(因?yàn)閷?shí)現(xiàn)了next方法)。實(shí)例變量x維護(hù)迭代器內(nèi)部的狀態(tài)衔统。每次調(diào)用next()方法的時(shí)候做兩件事:
- 為下一次調(diào)用next()方法修改狀態(tài)
- 為當(dāng)前這次調(diào)用生成返回結(jié)果
迭代器就像一個(gè)懶加載的工廠鹿榜,等到有人需要的時(shí)候才給它生成值返回,沒調(diào)用的時(shí)候就處于休眠狀態(tài)等待下一次調(diào)用锦爵。
生成器(generator)
我們先來看一段官方對生成器的說明:
Python’s generators provide a convenient way to implement the iterator protocol.
意思大概是犬缨,python的生成器是一種以更優(yōu)雅的方式去實(shí)現(xiàn)的迭代器,可見生成器是迭代器的一種棉浸。
生成器算得上是Python語言中最吸引人的特性之一怀薛,生成器其實(shí)是一種特殊的迭代器,不過這種迭代器更加優(yōu)雅迷郑。它不需要再像上面的類一樣寫iter()和next()方法了枝恋,只需要一個(gè)yiled關(guān)鍵字。 生成器一定是迭代器(反之不成立)嗡害,因此任何生成器也是以一種懶加載的模式生成值焚碌。用生成器實(shí)現(xiàn)上面迭代器的例子是:
def fib():
x = 0
while x<2 :
x += 1
yield x
運(yùn)行結(jié)果:
a = fib()
next(a)
Out[100]: 1
next(a)
Out[101]: 2
next(a)
Traceback (most recent call last):
File "<ipython-input-102-3f6e2eea332d>", line 1, in <module>
next(a)
StopIteration
type(a)
Out[103]: generator
fib就是一個(gè)普通的python函數(shù),它特殊的地方在于函數(shù)體中沒有return關(guān)鍵字霸妹,函數(shù)的返回值是一個(gè)生成器對象十电。當(dāng)執(zhí)行f=fib()返回的是一個(gè)生成器對象,此時(shí)函數(shù)體中的代碼并不會執(zhí)行叹螟,只有顯示或隱示地調(diào)用next的時(shí)候才會真正執(zhí)行里面的代碼鹃骂。
yield 的好處是顯而易見的,把一個(gè)函數(shù)改寫為一個(gè) generator 就獲得了迭代能力罢绽,比起用類的實(shí)例保存狀態(tài)來計(jì)算下一個(gè) next() 的值畏线,不僅代碼簡潔,而且執(zhí)行流程異常清晰良价。
生成器在Python中是一個(gè)非常強(qiáng)大的編程結(jié)構(gòu)寝殴,可以用更少地中間變量寫流式代碼蒿叠,此外,相比其它容器對象它更能節(jié)省內(nèi)存和CPU蚣常,當(dāng)然它可以用更少的代碼來實(shí)現(xiàn)相似的功能市咽。
生成器表達(dá)式(generator expression)
生成器表達(dá)式是列表推倒式的生成器版本,看起來像列表推導(dǎo)式抵蚊,但是它返回的是一個(gè)生成器對象而不是列表對象魂务。
a = (x for x in range(10))
type(a)
Out[106]: generator
next(a)
Out[108]: 0
列表推導(dǎo)式長什么樣(將小括號換成中括號就是一個(gè)列表推導(dǎo)式了):
b = [x for x in range(10)]
b
Out[110]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
深入學(xué)習(xí)yield(原文章地址)
def g():
print("1 is")
yield 1
print("2 is")
yield 2
print("3 is")
yield 3
>>>z = g()
>>>next(z)
1 is
1
>>>next(z)
2 is
2
>>>next(z)
3 is
3
>>>next(z)
Traceback (most recent call last):
File "<ipython-input-166-7b32f85a2b4e>", line 1, in <module>
next(z)
StopIteration
第一次調(diào)用next()方法時(shí),函數(shù)似乎執(zhí)行到y(tǒng)ield 1泌射,就暫停了粘姜。然后再次調(diào)用next()時(shí),函數(shù)從yield 1之后開始執(zhí)行的熔酷,并再次暫停孤紧。第三次調(diào)用next(),從第二次暫停的地方開始執(zhí)行拒秘。第四次,拋出StopIteration異常号显。
事實(shí)上,generator確實(shí)在遇到y(tǒng)ield之后暫停了躺酒,確切點(diǎn)說押蚤,是先返回了yield表達(dá)式的值,再暫停的羹应。當(dāng)再次調(diào)用next()時(shí)榕莺,從先前暫停的地方開始執(zhí)行绊寻,直到遇到下一個(gè)yield掀宋。這與上文介紹的對iterator調(diào)用next()方法戚丸,執(zhí)行原理一般無二。
有些教程里說generator保存的是算法裸违,而我覺得用中斷服務(wù)子程序來描述generator或許能更好理解掖桦,這樣你就能將yield理解成一個(gè)中斷服務(wù)子程序的斷點(diǎn),沒錯(cuò)供汛,是中斷服務(wù)子程序的斷點(diǎn)枪汪。我們每次對一個(gè)generator對象調(diào)用next()時(shí),函數(shù)內(nèi)部代碼執(zhí)行到”斷點(diǎn)”yield怔昨,然后返回這一部分的結(jié)果雀久,并保存上下文環(huán)境,”中斷”返回朱监。
我們再來看另一段代碼岸啡。
def gen():
while True:
s = yield
print(s)
>>> g = gen()
>>> g.send('hello') #這里很重要,向一個(gè)剛開始的生成器直接send一個(gè)值赫编,會報(bào)錯(cuò)巡蘸,所以我們得先調(diào)用next方法
#讓生成器向后移動一個(gè)位置后再send值
Traceback (most recent call last):
File "<ipython-input-169-9d017dfb1443>", line 1, in <module>
g.send('hello')
TypeError: can't send non-None value to a just-started generator
# 下面才是send函數(shù)正確的使用方法
>>> next(g)
>>> g.send('hello')
hello
我也是看到這個(gè)形式的generator,懵了擂送,才想要深入學(xué)習(xí)generator與yield的悦荒。結(jié)合以上的知識,我再告訴你嘹吨,generator其實(shí)有第2種調(diào)用方法(恢復(fù)執(zhí)行)搬味,即通過send(value)方法將value作為yield表達(dá)式的當(dāng)前值,你可以用該值再對其他變量進(jìn)行賦值蟀拷,這一段代碼就很好理解了碰纬。當(dāng)我們調(diào)用send(value)方法時(shí),generator正由于yield的緣故被暫停了问芬。此時(shí)悦析,send(value)方法傳入的值作為yield表達(dá)式的值,函數(shù)中又將該值賦給了變量s此衅,然后print函數(shù)打印s强戴,循環(huán)再遇到y(tǒng)ield,暫停返回挡鞍。
調(diào)用send(value)時(shí)要注意骑歹,要確保,generator是在yield處被暫停了墨微,如此才能向yield表達(dá)式傳值道媚,否則將會報(bào)錯(cuò)(如上所示),可通過next()方法或send(None)使generator執(zhí)行到y(tǒng)ield翘县。
再來看一段yield更復(fù)雜的用法衰琐,或許能加深你對generator的next()與send(value)的理解。
>>> def echo(value=None):
... while 1:
... value = (yield value)
... print("The value is", value)
... if value:
... value += 1
...
>>> g = echo(1)
>>> next(g)
1
>>> g.send(2)
The value is 2
3
>>> g.send(5)
The value is 5
6
>>> next(g)
The value is None
上述代碼既有yield value的形式炼蹦,又有value = yield形式羡宙,看起來有點(diǎn)復(fù)雜。但以yield分離代碼進(jìn)行解讀掐隐,就不太難了狗热。第一次調(diào)用next()方法,執(zhí)行到y(tǒng)ield value表達(dá)式虑省,保存上下文環(huán)境暫停返回1匿刮。第二次調(diào)用send(value)方法,從value = yield開始探颈,打印熟丸,再次遇到y(tǒng)ield value暫停返回。后續(xù)的調(diào)用send(value)或next()都不外如是伪节。
但是光羞,這里就引出了另一個(gè)問題绩鸣,yield作為一個(gè)暫停恢復(fù)的點(diǎn)纱兑,代碼從yield處恢復(fù)呀闻,又在下一個(gè)yield處暫停∏鄙鳎可見捡多,在一次next()(非首次)或send(value)調(diào)用過程中,實(shí)際上存在2個(gè)yield铐炫,一個(gè)作為恢復(fù)點(diǎn)的yield與一個(gè)作為暫停點(diǎn)的yield垒手。因此,也就有2個(gè)yield表達(dá)式倒信。send(value)方法是將值傳給恢復(fù)點(diǎn)yield;調(diào)用next()表達(dá)式的值時(shí)科贬,其恢復(fù)點(diǎn)yield的值總是為None,而將暫停點(diǎn)的yield表達(dá)式的值返回堤结。為方便記憶唆迁,你可以將此處的恢復(fù)點(diǎn)記作當(dāng)前的(current),而將暫停點(diǎn)記作下一次的(next)竞穷,這樣就與next()方法匹配起來啦唐责。
小結(jié)
- 可迭代對象(Iterable)是實(shí)現(xiàn)了iter()方法的對象,通過調(diào)用iter()方法可以獲得一個(gè)迭代器(Iterator)。
- 迭代器(Iterator)是實(shí)現(xiàn)了iter()和next()的對象瘾带。
- for … in …的迭代鼠哥,實(shí)際是將可迭代對象轉(zhuǎn)換成迭代器,再重復(fù)調(diào)用next()方法實(shí)現(xiàn)的看政。
- 生成器(generator)是一個(gè)特殊的迭代器朴恳,它的實(shí)現(xiàn)更簡單優(yōu)雅
- ieyield是生成器實(shí)現(xiàn)next()方法的關(guān)鍵。它作為生成器執(zhí)行的暫驮黍迹恢復(fù)點(diǎn)于颖,可以對yield表達(dá)式進(jìn)行賦值,也可以將yield表達(dá)式的值返回嚷兔。