搞清楚 Python 的迭代器涌萤、可迭代對(duì)象淹遵、生成器

很多伙伴對(duì) Python 的迭代器、可迭代對(duì)象负溪、生成器這幾個(gè)概念有點(diǎn)搞不清楚透揣,我來(lái)說(shuō)說(shuō)我的理解,希望對(duì)需要的朋友有所幫助川抡。

1 迭代器協(xié)議

迭代器協(xié)議是核心辐真,搞懂了這個(gè),上面的幾個(gè)概念也就很好理解了崖堤。

所謂迭代器協(xié)議侍咱,就是要求一個(gè)迭代器必須要實(shí)現(xiàn)如下兩個(gè)方法

iterator.__iter__()
Return the iterator object itself.
iterator.__next__()
Return the next item from the container.

也就是說(shuō),一個(gè)對(duì)象只要支持上面兩個(gè)方法密幔,就是迭代器楔脯。__iter__() 需要返回迭代器本身,而 __next__() 需要返回下一個(gè)元素胯甩。

2 可迭代對(duì)象

知道了迭代器的概念昧廷,那可迭代對(duì)象又是啥呢?

這個(gè)更簡(jiǎn)單偎箫,只要對(duì)象實(shí)現(xiàn)了 __iter__() 方法木柬,并且返回的是一個(gè)迭代器,那么這個(gè)對(duì)象就是可迭代對(duì)象镜廉。

比如我們常見(jiàn)的列表就是可迭代對(duì)象

>>> l = [1, 3, 5]
>>> iter(l)
<list_iterator object at 0x101a1d9e8>

使用 iter() 會(huì)調(diào)用對(duì)應(yīng)的 __iter__() 方法弄诲,這里返回的是一個(gè)列表迭代器,所以說(shuō)列表就是一個(gè)可迭代對(duì)象娇唯。

3 手寫(xiě)一個(gè)迭代器

迭代器的實(shí)現(xiàn)有不同的方式齐遵,相信大家首先能想到的就是自定義類,我們就從這個(gè)說(shuō)起塔插。

便于說(shuō)明梗摇,我們手寫(xiě)一個(gè)迭代器,用于生成奇數(shù)序列想许。

按照迭代器協(xié)議伶授,我們實(shí)現(xiàn)上述的兩個(gè)方法断序。

class Odd:
    def __init__(self, start=1):
        self.cur = start

    def __iter__(self):
        return self

    def __next__(self):
        ret_val = self.cur
        self.cur += 2
        return ret_val

終端里,我們實(shí)例化一個(gè) Odd 類得到一個(gè)對(duì)象 odd

>>> odd = Odd()
>>> odd
<__main__.Odd object at 0x101a1d9b0>

使用 iter() 方法會(huì)調(diào)用類里的 __iter__ 方法糜烹,得到它本身

>>> iter(odd)
<__main__.Odd object at 0x101a1d9b0>

使用 next() 方法會(huì)調(diào)用對(duì)應(yīng)的 __next__() 方法违诗,得到下一個(gè)元素

>>> next(odd)
1
>>> next(odd)
3
>>> next(odd)
5

其實(shí),odd 對(duì)象就是一個(gè)迭代器了疮蹦。

我們可以用 for 來(lái)遍歷它

odd = Odd()
for v in odd:
    print(v)

細(xì)心的伙伴可能會(huì)發(fā)現(xiàn)诸迟,這個(gè)其實(shí)會(huì)無(wú)限的打印下去,那怎么解決呢愕乎?

我們拿一個(gè)列表做做實(shí)驗(yàn)阵苇,先得到它的迭代器對(duì)象

>>> l = [1, 3, 5]
>>> li = iter(l)
>>> li
<list_iterator object at 0x101a1da90>

然后手動(dòng)獲取下一個(gè)元素,直到?jīng)]有下一個(gè)元素為止感论,看下會(huì)發(fā)生什么

>>> next(li)
1
>>> next(li)
3
>>> next(li)
5
>>> next(li)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

原來(lái)列表迭代器會(huì)在沒(méi)有下一個(gè)元素的時(shí)候拋出 StopIteration 異常绅项,估計(jì) for 語(yǔ)句就是根據(jù)這個(gè)異常來(lái)確定是否結(jié)束。

我們修改一下原來(lái)的代碼比肄,能生成指定范圍內(nèi)的奇數(shù)

class Odd:
    def __init__(self, start=1, end=10):
        self.cur = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.cur > self.end:
            raise StopIteration
        ret_val = self.cur
        self.cur += 2
        return ret_val

我們使用 for 試一下

>>> odd = Odd(1, 10)
>>> for v in odd:
...     print(v)
...
1
3
5
7
9

果然快耿,和預(yù)期一致。

我們用 while 循環(huán)模擬 for 的執(zhí)行過(guò)程

目標(biāo)代碼

for v in iterable:
    print(v)

翻譯后的代碼

iterator = iter(iterable)
while True:
    try:
        v = next(iterator)
        print(v)
    except StopIteration:
        break

事實(shí)上 Python 的 for 語(yǔ)句原理也就是這樣薪前,可以將 for 理解為一個(gè)語(yǔ)法糖润努。

4 創(chuàng)建迭代器的其它方式

生成器其實(shí)也是迭代器,所以可以使用生成器的創(chuàng)建方式創(chuàng)建迭代器示括。

4.1 生成器函數(shù)

和普通函數(shù)的 return 返回不同,生成器函數(shù)使用 yield痢畜。

>>> def odd_func(start=1, end=10):
...     for val in range(start, end + 1):
...         if val % 2 == 1:
...             yield val
...
>>> of = odd_func(1, 5)
>>> of
<generator object odd_func at 0x101a14200>
>>> iter(of)
<generator object odd_func at 0x101a14200>
>>> next(of)
1
>>> next(of)
3
>>> next(of)
5
>>> next(of)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

4.2 生成器表達(dá)式

>>> g = (v for v in range(1, 5 + 1) if v % 2 == 1)
>>> g
<generator object <genexpr> at 0x101a142b0>
>>> iter(g)
<generator object <genexpr> at 0x101a142b0>
>>> next(g)
1
>>> next(g)
3
>>> next(g)
5
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

4.3 怎么選擇

到現(xiàn)在為止垛膝,我們知道了創(chuàng)建迭代器的 3 種方式,那么該如何選擇丁稀?

不用說(shuō)也知道吼拥,最簡(jiǎn)單的就是生成器表達(dá)式,如果表達(dá)式能滿足需求线衫,那么就是它凿可;如果需要添加比較復(fù)雜的邏輯就選生成器函數(shù);如果前兩者沒(méi)法滿足需求授账,那就自定義類實(shí)現(xiàn)吧枯跑。總之白热,選擇最簡(jiǎn)單的方式就行敛助。

5 迭代器的特點(diǎn)

5.1 惰性

迭代器并不是把所有的元素提前計(jì)算出來(lái),而是在需要的時(shí)候才計(jì)算返回屋确。

5.2 支持無(wú)限個(gè)元素

比如上面我們建立的第一個(gè) Odd 類纳击,它的實(shí)例 odd 表示大于 start 的所有奇數(shù)续扔,而列表等容器沒(méi)法容納無(wú)限個(gè)元素的。

5.3 省空間

比如存 10000 個(gè)元素

>>> from sys import getsizeof
>>> a = [1] * 10000
>>> getsizeof(a)
80064

列表占用 80K 左右焕数。

而迭代器呢纱昧?

>>> from itertools import repeat
>>> b = repeat(1, times=10000)
>>> getsizeof(b)
56

只占用了 56 個(gè)字節(jié)。

也正因?yàn)榈鞫栊缘奶攸c(diǎn)堡赔,才有了這個(gè)優(yōu)勢(shì)识脆。

6 一些需要注意的細(xì)節(jié)

6.1 迭代器同時(shí)也是可迭代對(duì)象

因?yàn)榈鞯?__iter__() 方法返回了它自身,而正好它本身就是個(gè)迭代器加匈,所以說(shuō)迭代器也是可迭代對(duì)象存璃。

6.2 迭代器遍歷完一次就不能從頭開(kāi)始了

看一個(gè)奇怪的例子

>>> l = [1, 3, 5]
>>> li = iter(l)
>>> li
<list_iterator object at 0x101a1da90>
>>> 3 in li
True
>>> 3 in li
False

因?yàn)?li 是列表迭代器,第一次查找 3 的時(shí)候雕拼,找到了纵东,所以返回 True,但是由于第一次迭代啥寇,已經(jīng)跳過(guò)了 3 那個(gè)元素偎球,第二次就找不到了,所以會(huì)出現(xiàn) False辑甜。

因此衰絮,記得迭代器是「一次性」的。

當(dāng)然磷醋,列表是可迭代對(duì)象猫牡,不管查找?guī)状味际钦5摹#ú缓美斫獾脑挼讼撸胂肷厦?for 語(yǔ)句的執(zhí)行原理淌友,每次都會(huì)從可迭代對(duì)象那通過(guò) iter() 方法取到新的迭代器)

>>> 3 in l
True
>>> 3 in l
True

7 小節(jié)

  • 實(shí)現(xiàn)了迭代器協(xié)議的對(duì)象都是迭代器
  • 實(shí)現(xiàn)了 __iter__() 方法并返回迭代器的對(duì)象是可迭代對(duì)象
  • 生成器也是一種迭代器
  • 創(chuàng)建迭代器有三種方式,生成器表達(dá)式骇陈、生成器函數(shù)震庭、自定義類,看情況選擇最簡(jiǎn)單的就好
  • 迭代器同時(shí)也是可迭代對(duì)象
  • 迭代器是「一次性」的

前面 3 小項(xiàng)是重點(diǎn)你雌,這 3 點(diǎn)理解了器联,其它的也都能領(lǐng)會(huì)。搞清楚標(biāo)題的那幾個(gè)名詞的概念的自然也沒(méi)有問(wèn)題婿崭。

8 參考

本文首發(fā)于公眾號(hào)「小小后端」拨拓,關(guān)注并回復(fù)「HMPython2018」領(lǐng)取 18 年很贊的 Python 學(xué)習(xí)教程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末逛球,一起剝皮案震驚了整個(gè)濱河市千元,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颤绕,老刑警劉巖幸海,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祟身,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡物独,警方通過(guò)查閱死者的電腦和手機(jī)袜硫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)挡篓,“玉大人婉陷,你說(shuō)我怎么就攤上這事」傺校” “怎么了秽澳?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)戏羽。 經(jīng)常有香客問(wèn)我担神,道長(zhǎng),這世上最難降的妖魔是什么始花? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任妄讯,我火速辦了婚禮,結(jié)果婚禮上酷宵,老公的妹妹穿的比我還像新娘亥贸。我一直安慰自己,他們只是感情好浇垦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布炕置。 她就那樣靜靜地躺著,像睡著了一般男韧。 火紅的嫁衣襯著肌膚如雪讹俊。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,718評(píng)論 1 305
  • 那天煌抒,我揣著相機(jī)與錄音,去河邊找鬼厕倍。 笑死寡壮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的讹弯。 我是一名探鬼主播况既,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼组民!你這毒婦竟也來(lái)了棒仍?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤臭胜,失蹤者是張志新(化名)和其女友劉穎莫其,沒(méi)想到半個(gè)月后癞尚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乱陡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年浇揩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片憨颠。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡胳徽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出爽彤,到底是詐尸還是另有隱情养盗,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布适篙,位于F島的核電站往核,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏匙瘪。R本人自食惡果不足惜铆铆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丹喻。 院中可真熱鬧薄货,春花似錦、人聲如沸碍论。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鳍悠。三九已至税娜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間藏研,已是汗流浹背敬矩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蠢挡,地道東北人弧岳。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像业踏,于是被迫代替她去往敵國(guó)和親禽炬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355