python迭代對象估蹄,迭代器塑煎,生成器,以及yield用法詳解(轉(zhuǎn))

原文章地址: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)了iternext()(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í)候做兩件事:

  1. 為下一次調(diào)用next()方法修改狀態(tài)
  2. 為當(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é)

  1. 可迭代對象(Iterable)是實(shí)現(xiàn)了iter()方法的對象,通過調(diào)用iter()方法可以獲得一個(gè)迭代器(Iterator)。
  2. 迭代器(Iterator)是實(shí)現(xiàn)了iter()和next()的對象瘾带。
  3. for … in …的迭代鼠哥,實(shí)際是將可迭代對象轉(zhuǎn)換成迭代器,再重復(fù)調(diào)用next()方法實(shí)現(xiàn)的看政。
  4. 生成器(generator)是一個(gè)特殊的迭代器朴恳,它的實(shí)現(xiàn)更簡單優(yōu)雅
  5. ieyield是生成器實(shí)現(xiàn)next()方法的關(guān)鍵。它作為生成器執(zhí)行的暫驮黍迹恢復(fù)點(diǎn)于颖,可以對yield表達(dá)式進(jìn)行賦值,也可以將yield表達(dá)式的值返回嚷兔。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末森渐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子冒晰,更是在濱河造成了極大的恐慌同衣,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件壶运,死亡現(xiàn)場離奇詭異耐齐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進(jìn)店門埠况,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耸携,“玉大人,你說我怎么就攤上這事询枚∥シ” “怎么了浙巫?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵金蜀,是天一觀的道長。 經(jīng)常有香客問我的畴,道長渊抄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任丧裁,我火速辦了婚禮护桦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘煎娇。我一直安慰自己二庵,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布缓呛。 她就那樣靜靜地躺著催享,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哟绊。 梳的紋絲不亂的頭發(fā)上因妙,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機(jī)與錄音票髓,去河邊找鬼攀涵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛洽沟,可吹牛的內(nèi)容都是我干的以故。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼裆操,長吁一口氣:“原來是場噩夢啊……” “哼怒详!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起跷车,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤棘利,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后朽缴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體善玫,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茅郎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜗元。...
    茶點(diǎn)故事閱讀 38,809評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖系冗,靈堂內(nèi)的尸體忽然破棺而出奕扣,到底是詐尸還是另有隱情,我是刑警寧澤掌敬,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布惯豆,位于F島的核電站,受9級特大地震影響奔害,放射性物質(zhì)發(fā)生泄漏楷兽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一华临、第九天 我趴在偏房一處隱蔽的房頂上張望芯杀。 院中可真熱鬧,春花似錦雅潭、人聲如沸揭厚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽筛圆。三九已至,卻和暖如春诚欠,著一層夾襖步出監(jiān)牢的瞬間顽染,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工轰绵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留粉寞,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓左腔,卻偏偏與公主長得像唧垦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子液样,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容