Python進(jìn)階8

迭代器和生成器

python的迭代協(xié)議

引言

  • 迭代器是訪問集合內(nèi)部元素的一種方式介陶,一般用來遍歷數(shù)據(jù)以舒。
  • 迭代器和用下標(biāo)索引訪問的方式不一樣,迭代器是不能返回值的
  • 迭代器提供了一種惰性訪問數(shù)據(jù)的方式钉鸯,需要的時候才產(chǎn)生數(shù)據(jù)咬最。
  • 可迭代類型都實現(xiàn)了迭代協(xié)議吠各,實際上就是__iter__()這個魔法函數(shù)。

可迭代類型和迭代器

前面講過星瘾,collections.abc模塊中定義了很多內(nèi)置的抽象基類走孽,現(xiàn)在我們重點關(guān)注其中的兩個:IterableIterator

Iterable

image

里面定義了一個抽象方法,__iter__()琳状,也就是說某個類只要實現(xiàn)了這個魔法函數(shù)磕瓷,它就是可迭代的類型

Iterator

image

首先,Iterator繼承了Iterable念逞,在它的基礎(chǔ)上困食,又增加了一個抽象方法:__next__(),它是用來讓迭代器獲取下一個元素翎承。

小結(jié)

可迭代類型迭代器并不一樣硕盹,前者只需要實現(xiàn)__iter__()函數(shù),而對后者而言叨咖,__next__()才是它的核心瘩例。

比如list類型,它是一個可迭代類型甸各,但并不是一個迭代器垛贤。

a = [1, 2, 3]
print(isinstance(a, Iterable), isinstance(a, Iterator))

# result:
# True False

補(bǔ)充

魔法函數(shù)那一小節(jié),我們講過這樣一個例子:

class Language(object):
    def __init__(self, language_list):
        self.lans = language_list
        
    def __getitem__(self, item):
        return self.lans[item]

language = Language(["Python", "C", "Lisp"])
for lan in language:
    print(lan)

# result:
# Python
# C
# Lisp

Language這個類中趣倾,我們定義的是__getitem__這個魔法函數(shù)聘惦,然后對這個類產(chǎn)生的實例我們可以使用for來遍歷元素了。也就是說它成為了一個可迭代類型儒恋,但它并沒有實現(xiàn)剛才我們討論的__iter__()函數(shù)善绎。

這是因為,在Python內(nèi)部诫尽,很多地方做了兼容處理禀酱,當(dāng)我們是用for進(jìn)行迭代遍歷,解釋器首先會尋找__iter__()函數(shù)牧嫉,如果沒有剂跟,它就會退一步去尋找__getitem__(),這個是序列類型中會實現(xiàn)的一個魔法函數(shù)驹止,只要它接收從 0 開始的整數(shù)為參數(shù),這個對象也是會被當(dāng)做可迭代類型的观蜗。

實際上臊恋,僅僅滿足了可迭代類型還不夠,真正能進(jìn)行迭代取值的是迭代器墓捻。通過iter()函數(shù)抖仅,我們可以返回一個可迭代對象的迭代器坊夫,有了它才能進(jìn)行迭代取值。

class Language(object):
    def __init__(self, language_list):
        self.lans = language_list
        
    def __getitem__(self, item):
        return self.lans[item]

language = Language(["Python", "C", "Lisp"])
my_iterator = iter(language)

print(my_iterator)

# result:
# <iterator object at 0x000002043CEE4F28>

如果我們不實現(xiàn)__iter__()__getitem__()撤卢,獲取迭代器的過中會出錯

class Language(object):
    def __init__(self, language_list):
        self.lans = language_list

language = Language(["Python", "C", "Lisp"])
my_iterator = iter(language)

print(my_iterator)

# result:
# TypeError: 'Language' object is not iterable

有了迭代器环凿,迭代取值需要另外一個函數(shù)next(),每調(diào)用一次放吩,就會返回一個值智听,直到拋出一個迭代結(jié)束的異常。

class Language(object):
    def __init__(self, language_list):
        self.lans = language_list
        
    def __getitem__(self, item):
        return self.lans[item]

language = Language(["Python", "C", "Lisp"])
my_iterator = iter(language)

print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))

# result:
# Python
# C
# Lisp
# StopIteration

上面的結(jié)果已經(jīng)很接近直接使用for進(jìn)行迭代了渡紫,但是到推,依賴__getitem__()函數(shù),底層還是隱藏了很多細(xì)節(jié)惕澎,如果我們想純粹地通過__iter__()來實現(xiàn)迭代過程莉测,要怎么做呢?

__iter__()用來返回一個迭代器唧喉,通過這個迭代器來迭代取值捣卤,對應(yīng)顯示調(diào)用iter()的邏輯。

__next__()用來讓迭代器取下一個值八孝,對應(yīng)顯示調(diào)用next()的邏輯董朝。

使用for的時候,這兩個魔法函數(shù)會被自動調(diào)用唆阿,完成迭代取值過程益涧。

from collections.abc import Iterator

class MyIterator(Iterator):
    def __init__(self, data_list):
        self.iter_list = data_list
        self.index = 0
    
    def __next__(self):
        # 這里是通過記錄索引,單次取值達(dá)到迭代目的
        # 更好的方式是通過生成器來進(jìn)行迭代取值
        try:
            data = self.iter_list[self.index]
        except IndexError:
            raise StopIteration
        self.index += 1
        return data

class Language(object):
    def __init__(self, language_list):
        self.lans = language_list
        
    def __iter__(self):
        return MyIterator(self.lans)
        

language = Language(["Python", "C", "Lisp"])
for lan in language:
    print(lan)
    
# result:
# Python
# C
# Lisp

生成器函數(shù)使用

引言

  • 函數(shù)里面只要存在yield關(guān)鍵字驯鳖,它就是生成器函數(shù)
  • 生成器是惰性計算的一個關(guān)鍵

使用案例

def gen_func():
    yield "MetaTian"
    
def func():
    return "MetaTian"
    
gen, res = gen_func(), func()
print(gen)
print(res)

# result:
# <generator object gen_func at 0x000001B102C3C0F8>
# MetaTian

for val in gen:
    print(val)
    
# result:
# MetaTian

第一個函數(shù)返回的是一個生成器對象闲询,它是一個可迭代類型,因此浅辙,可以通過for進(jìn)行訪問扭弧。

def gen_func():
    yield 1
    yield 2
    yield 3
    
gen = gen_func()
for val in gen:
    print(val)
    
# result:
# 1
# 2
# 3

生成器的原理

Python中函數(shù)工作原理

對于編譯型語言,函數(shù)的調(diào)用會維持一個函數(shù)調(diào)用棧记舆,某個函數(shù)執(zhí)行完成后鸽捻,它就會被出棧處理,也就是說泽腮,函數(shù)執(zhí)行后御蒲,它的生命周期就結(jié)束了。

對于Python這樣的解釋型語言诊赊,函數(shù)的調(diào)用也要依賴棧結(jié)構(gòu)厚满,但是,函數(shù)對象是存放在堆內(nèi)存中的碧磅,也就意味著碘箍,一個函數(shù)被調(diào)用執(zhí)行了遵馆,它還在那兒。

什么是堆內(nèi)存和棧內(nèi)存丰榴?

解釋器用一個叫做PyEval_EvalFrameEx的C函數(shù)來執(zhí)行Python程序货邓。對于一個Python中的函數(shù),解釋器接受一個棧幀(stack frame)對象四濒,并在這個棧幀的上下文中執(zhí)行Python字節(jié)碼换况,完成函數(shù)調(diào)用。在字節(jié)碼執(zhí)行中峻黍,如果遇到了要調(diào)用其他函數(shù)的指令复隆,解釋器會創(chuàng)建一個新的棧幀用來執(zhí)行新調(diào)用的函數(shù)。

生成器+函數(shù)

def gen_func():
    yield 1
    name = "MetaTian"
    yield 2
    
    return "done"

Python將函數(shù)編譯為字節(jié)碼時姆涩,如果遇到yield關(guān)鍵字挽拂,它就知道這是一個生成器函數(shù),內(nèi)部會做一個標(biāo)記骨饿。當(dāng)我們調(diào)用這個函數(shù)的時候亏栈,解釋器看到這個標(biāo)記后就會創(chuàng)建一個生成器,而不是去運(yùn)行它宏赘,后續(xù)函數(shù)的執(zhí)行交給生成器控制绒北。

這個生成器內(nèi)部有兩個東西,一是對棧幀的引用察署,二是函數(shù)字節(jié)碼的引用闷游。棧幀中有一個指針,指向最近執(zhí)行的那條指令贴汪,因為執(zhí)行到和yield有關(guān)的字節(jié)碼后脐往,函數(shù)會停止執(zhí)行,相當(dāng)于打了個斷點扳埂,同時將yield后面的值返回业簿。通過next(),可以讓函數(shù)繼續(xù)執(zhí)行(因為生成器也是迭代器)阳懂,直到遇到下一個yield梅尤。

生成器對象也是分配在堆內(nèi)存中的,也就是說岩调,只要我們在程序運(yùn)行的任何地方拿到了這個對象巷燥,都可以用它來控制函數(shù)的執(zhí)行。這也是后面攜程的一個理論基礎(chǔ)号枕。

重構(gòu)自己的可迭代類型

引言

  • 前面將Language這個類缰揪,構(gòu)建成為了我們自定義的一個可迭代類型
  • 生成器也是迭代器堕澄,通過使用生成器邀跃,可以更簡潔地達(dá)成目的。
from collections.abc import Iterator


def gen_func():
    yield "MetaTian"
    
gen = gen_func()
print(isinstance(gen, Iterator))

# result:
# True

使用案例

class Language(object):
    def __init__(self, language_list):
        self.lans = language_list
        
    def __iter__(self):
        i = 0
        try:
            while True:
                val = self.lans[i]
                yield val
                i += 1
        except IndexError:
            return
    
language = Language(["Python", "C", "Lisp"])
for lan in language:
    print(lan)
    
# result:
# Python
# C
# Lisp

總結(jié)

這里再來回顧一下前面講過的內(nèi)容蛙紫,使用for遍歷的時候拍屑,首先會看作用對象是否為一個可迭代類型,如果是坑傅,那么會隱式調(diào)用__iter__()僵驰,得到一個迭代器對象,有了它唁毒,再隱式調(diào)用__next__()來不斷獲取下一個元素蒜茴,直到遇到一個停止迭代的異常。我們也可以通過內(nèi)置的兩個函數(shù)iter()next()來人工干預(yù)迭代的過程浆西。

Language類中粉私,__iter__()內(nèi)部加入了一個生成器的邏輯,結(jié)合前面的生成器函數(shù)近零,可以知道诺核,遇到yield語句后,會產(chǎn)生一個生成器對象久信,由它來控制這個函數(shù)的后續(xù)執(zhí)行窖杀。

因為生成器也是迭代器,所以__next__()的邏輯對它同樣適用裙士,每次 next 都會在__iter__()函數(shù)中的while循環(huán)中不斷取值入客,直到拋出一個IndexError,迭代結(jié)束腿椎,__iter__()函數(shù)結(jié)束桌硫,for邏輯完成。

生成器讀取大文件

引言

  • 有一個數(shù)據(jù)文件酥诽,大小為 10GB
  • 數(shù)據(jù)只有一行鞍泉,行中有特殊的分隔符,現(xiàn)在需要剔除分隔符肮帐,獲得每一個被分隔的元素咖驮。
比如,數(shù)據(jù)文件長這樣:
dj134o0kgfdkjfkdjfk<end>'6823sdkfslkfsldkfj'<end>sdkfslfjyerojskfj<end>...

<end> 是其中的分隔符

實現(xiàn)過程

def extract(f, sep):
    buff = ""
    
    while True:
        block = f.read(1024*4)
        if not block:   # 沒讀到內(nèi)容训枢,說明讀到尾部了
            yield buff      # 上次留下來的內(nèi)容
            break
        
        buff += block
        while sep in buff:
            cut = buff.index(sep)   # 定位
            yield buff[:cut]    # 分隔符前的一個元素
            buff = buff[cut + len(sep)]     # 跳過分隔符和前面的元素


with open("data.txt") as f:
    for line in extract(f, "<end>"):
        print(line)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末托修,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子恒界,更是在濱河造成了極大的恐慌睦刃,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件十酣,死亡現(xiàn)場離奇詭異涩拙,居然都是意外死亡际长,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門兴泥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來工育,“玉大人,你說我怎么就攤上這事搓彻∪绯瘢” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵旭贬,是天一觀的道長怔接。 經(jīng)常有香客問我,道長稀轨,這世上最難降的妖魔是什么扼脐? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮奋刽,結(jié)果婚禮上谎势,老公的妹妹穿的比我還像新娘。我一直安慰自己杨名,他們只是感情好脏榆,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著台谍,像睡著了一般须喂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上趁蕊,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天坞生,我揣著相機(jī)與錄音,去河邊找鬼掷伙。 笑死是己,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的任柜。 我是一名探鬼主播卒废,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼宙地!你這毒婦竟也來了摔认?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤宅粥,失蹤者是張志新(化名)和其女友劉穎参袱,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡抹蚀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年剿牺,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片环壤。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡牢贸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出镐捧,到底是詐尸還是另有隱情,我是刑警寧澤臭增,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布懂酱,位于F島的核電站,受9級特大地震影響誊抛,放射性物質(zhì)發(fā)生泄漏列牺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一拗窃、第九天 我趴在偏房一處隱蔽的房頂上張望瞎领。 院中可真熱鬧,春花似錦随夸、人聲如沸九默。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驼修。三九已至,卻和暖如春诈铛,著一層夾襖步出監(jiān)牢的瞬間乙各,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工幢竹, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留耳峦,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓焕毫,卻偏偏與公主長得像蹲坷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子邑飒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

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