深入理解python虛擬機(jī)生成器停止背后原理
深入理解 python 虛擬機(jī):生成器停止背后的魔法
在本篇文章當(dāng)中主要給大家介紹 Python 當(dāng)中生成器的實(shí)現(xiàn)原理,尤其是生成器是如何能夠被停止執(zhí)行,而且還能夠被恢復(fù)的,這是一個(gè)非常讓人疑惑的地方。因?yàn)檫@與我們通常使用的函數(shù)的直覺是相違背的,函數(shù)之后執(zhí)行完成之后才會(huì)返回,而生成表面是函數(shù)的形式,但是這違背了我們正常的編程直覺。
深入理解生成器與函數(shù)的區(qū)別
為了從根本上建立對(duì)生成器的認(rèn)識(shí),我們首先就需要深入理解一下生成器和函數(shù)的區(qū)別。其實(shí)在從虛擬機(jī)的層面來看,他們兩個(gè)都是對(duì)象,只不過一個(gè)是生成器對(duì)象,一個(gè)是函數(shù)對(duì)象。在 Python 當(dāng)中,如果你在函數(shù)里面使用了 yield 語句,那么你的這個(gè)函數(shù)在被調(diào)用的時(shí)候就不會(huì)被執(zhí)行,而是會(huì)返回一個(gè)生成器對(duì)象。
>>> def bar(): ... print("before yield") ... res = yield 1 ... print(f"{res = }") ... print("after yield") ... return "Return Value" ... >>> generator = bar() >>> generator <generator object bar at 0x105267510> >>> bar <function bar at 0x10562fc40> >>>
在 Python 當(dāng)中有的對(duì)象是可以直接調(diào)用的,比如你自己的類如果實(shí)現(xiàn)了__call__
方法的話,這個(gè)類生成的對(duì)象就是一個(gè)可調(diào)用對(duì)象,在 Python 當(dāng)中一個(gè)最常見的可調(diào)用對(duì)象就是函數(shù)了,生成器和函數(shù)的區(qū)別之一就是,生成器不能夠直接被調(diào)用,而函數(shù)可以。
>>> generator() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'generator' object is not callable >>>
在上面的代碼當(dāng)中我們要明確 bar 是一個(gè)函數(shù),但是這個(gè)函數(shù)和正常的函數(shù)有一點(diǎn)區(qū)別,這個(gè)函數(shù)在被調(diào)用的時(shí)候不會(huì)直接執(zhí)行代碼,而是會(huì)返回一個(gè)生成器對(duì)象,因?yàn)樵谶@個(gè)函數(shù)體當(dāng)中使用了 yield 語句,我們稱這種函數(shù)為生成器函數(shù) (generator function),在 Python 當(dāng)中你可以通過查看一個(gè)函數(shù)的 co_flags 字段查看一個(gè)函數(shù)的屬性,如果這個(gè)字段和 0x0020 進(jìn)行 & 操作之后的結(jié)果大于 0,那么就說明這個(gè)函數(shù)是一個(gè)生成器函數(shù)。
>>> (bar.__code__.co_flags & 0x0020) > 0 True >>> bar.__code__.co_flags & 0x0020 32
從上面的代碼當(dāng)中我們可以看到 bar 就是一個(gè)生成器函數(shù),除了上面的方法 Python 的標(biāo)準(zhǔn)庫也提供了方法去輔助我們進(jìn)行判斷。
>>> import inspect >>> inspect.isgeneratorfunction(bar) True
上面的特性在 Python 程序進(jìn)行編譯的時(shí)候,編譯器可以做到這一點(diǎn),當(dāng)發(fā)現(xiàn)一個(gè)函數(shù)當(dāng)中存在類似 yield 的語句的時(shí)候就在函數(shù)的 co_flags 字段當(dāng)中和 0x0020 進(jìn)行或操作,然后將這個(gè)值保存在 co_flags 當(dāng)中。
總之生成器和函數(shù)之間的關(guān)系為:生成器對(duì)象是通過調(diào)用生成器函數(shù)得到的,調(diào)用生成器函數(shù)的返回對(duì)象是生成器。
虛實(shí)交錯(cuò)的時(shí)空魔法
首先我們需要了解的是,如果我們想讓一個(gè)生成器對(duì)象執(zhí)行下去的話,我們可以使用 next 或者 send 函數(shù),進(jìn)行實(shí)現(xiàn):
>>> next(generator) before yield 1 >>> next(generator) res = None after yield Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration: Return Value
在 CPython 實(shí)現(xiàn)的虛擬機(jī)當(dāng)中,如果我們想要正確的使用 send 函數(shù)首先需要讓生成器對(duì)象執(zhí)行到第一個(gè) yield 語句,我們可以使用 next(generator)
或者 generator.send(None)
。比如在上面的第一條語句當(dāng)中執(zhí)行 next(generator)
,運(yùn)行到語句 res = yield 1
,但是這條語句還沒有執(zhí)行完,需要我們調(diào)用 send 函數(shù)之后才能夠完成賦值操作,send 函數(shù)的參數(shù)會(huì)被賦值給變量 res 。當(dāng)整個(gè)函數(shù)體執(zhí)行完成之后虛擬機(jī)就會(huì)拋出 StopIteration 異常,并且將返回值保存到 StopIteration 異常對(duì)象當(dāng)中:
>>> generator = bar() >>> next(generator) before yield 1 >>> try: ... generator.send("None") ... except StopIteration as e: ... print(f"{e.value = }") ... res = 'None' after yield e.value = 'Return Value' >>>
上面的代碼當(dāng)中可以看到,我們正確的執(zhí)行力我們?cè)谏厦嬲劦降纳善鞯氖褂梅椒ǎ⑶覍⑸善鲌?zhí)行完成之后的返回值保存到異常的 value 當(dāng)中。
生成器內(nèi)部實(shí)現(xiàn)原理
從上面的關(guān)于生成器的使用方式來看,生成器可以在函數(shù)執(zhí)行到一半的時(shí)候停止,然后繼續(xù)恢復(fù)執(zhí)行,為了實(shí)現(xiàn)這一點(diǎn)我們就需要有一種手段去保存函數(shù)執(zhí)行的狀態(tài)。但是我們需要保存函數(shù)執(zhí)行的那些狀態(tài)呢?最重要的兩點(diǎn)就是代碼現(xiàn)在執(zhí)行到什么位置了,因?yàn)槲覀冎笠^續(xù)從下一條指令開始恢復(fù)執(zhí)行,同時(shí)我們需要保存虛擬機(jī)的??臻g,就是在執(zhí)行字節(jié)碼的時(shí)候使用到的 valuestack,注意這不是棧幀,同時(shí)還有執(zhí)行函數(shù)的局部變量表,這里主要是保存一些局部變量的。而這些東西都保存在虛擬機(jī)的棧幀當(dāng)中了,這一點(diǎn)我們?cè)谇懊娴奈恼庐?dāng)中已經(jīng)詳細(xì)介紹過了。
因此根據(jù)這些分析我們應(yīng)該知道了,生成器里面最重要的就是一個(gè)虛擬機(jī)的棧幀數(shù)據(jù)結(jié)構(gòu)了。一個(gè)生成器對(duì)象當(dāng)中一定需要有一個(gè)虛擬機(jī)的棧幀,在 CPython 的實(shí)現(xiàn)當(dāng)中,生成器對(duì)象的數(shù)據(jù)結(jié)構(gòu)如下:
typedef struct { /* The gi_ prefix is intended to remind of generator-iterator. */ PyObject ob_base; struct _frame *gi_frame; char gi_running; PyObject *gi_code; PyObject *gi_weakreflist; PyObject *gi_name; PyObject *gi_qualname; _PyErr_StackItem gi_exc_state; } PyGenObject;
- gi_frame: 這個(gè)字段就是表示生成器所擁有的棧幀。
- gi_running: 表示生成器是否在運(yùn)行。
- gi_code: 表示對(duì)應(yīng)生成器函數(shù)的代碼(字節(jié)碼)。
- gi_weakreflist: 用于保存這個(gè)棧幀對(duì)象保存的弱引用對(duì)象。
- gi_name 和 gi_qualname 都是表示生成器的名字,后者更加詳細(xì)。
- gi_exc_state: 用于保存執(zhí)行生成器代碼之前的程序狀態(tài),因?yàn)橹暗拇a可能已經(jīng)產(chǎn)生一些異常了,這個(gè)主要用于保存之前的程序狀態(tài),待生成器返回之后就進(jìn)行恢復(fù)。
class A: def hello(self): yield 1 if __name__ == '__main__': g = A().hello() print(g.__name__) print(g.__qualname__)
上面的程序輸出結(jié)果為:
hello
A.hello
生成器對(duì)應(yīng)的字節(jié)碼行為
我們通過下面的例子來分析一下,生成器 yield 對(duì)應(yīng)的字節(jié)碼:
>>> import dis >>> def hello(): ... yield 1 ... yield 2 ... >>> dis.dis(hello) 2 0 LOAD_CONST 1 (1) 2 YIELD_VALUE 4 POP_TOP 3 6 LOAD_CONST 2 (2) 8 YIELD_VALUE 10 POP_TOP 12 LOAD_CONST 0 (None) 14 RETURN_VALUE
在上面的程序當(dāng)中只有和生成器相關(guān)的字節(jié)碼為 YIELD_VALUE,在加載完常量 1 之后就會(huì)執(zhí)行 YIELD_VALUE 指令,虛擬機(jī)在執(zhí)行完 yield 指令之后,就會(huì)直接返回,此時(shí)虛擬機(jī)的狀態(tài)——valuestack 和當(dāng)前指令執(zhí)行的位置(在上面的這個(gè)例子當(dāng)中就是 4)都會(huì)被保存到虛擬機(jī)棧幀當(dāng)中,當(dāng)下一次執(zhí)行生成器的代碼的時(shí)候就會(huì)直接從 POP_TOP 指令直接執(zhí)行。
我們?cè)賮砜匆幌铝硗庖粋€(gè)比較重要的指令 YIELD_FROM:
>>> def generator_b(gen): ... yield from gen ... >>> dis.dis(generator_b) 2 0 LOAD_FAST 0 (gen) 2 GET_YIELD_FROM_ITER 4 LOAD_CONST 0 (None) 6 YIELD_FROM 8 POP_TOP 10 LOAD_CONST 0 (None) 12 RETURN_VALUE
我們現(xiàn)在用一個(gè)簡(jiǎn)單的例子重新回顧一下程序的行為:
def generator_a(): yield 1 yield 2 def generator_b(gen): yield from gen if __name__ == '__main__': gen = generator_b(generator_a()) print(gen.send(None)) print(gen.send(None)) try: gen.send(None) except StopIteration: print("generator exit")
上面的程序輸出結(jié)果如下所示:
1
2
generator exit
從上面程序的輸出結(jié)果我們可以看到 generator_a 的兩個(gè)值都會(huì)被返回,這些魔法隱藏在字節(jié)碼 YIELD_FROM 當(dāng)中。YIELD_FROM 字節(jié)碼會(huì)調(diào)用棧頂上的生成器對(duì)象的 send 方法,并且將參數(shù)生成器對(duì)象 gen 的返回結(jié)果返回,比如 1 和 2 這兩個(gè)值會(huì)被返回到 generator_b ,然后 generator_b 會(huì)將這個(gè)結(jié)果繼續(xù)傳播出來。
- 在這個(gè)字節(jié)碼執(zhí)行最后會(huì)進(jìn)行判斷虛擬機(jī)當(dāng)中是否出現(xiàn)了 StopIteration 異常,如果出現(xiàn)了則說 yield from 的生成器已經(jīng)執(zhí)行完了,則 generator_b 繼續(xù)往下執(zhí)行。
- 如果沒有 StopIteration 異常,則說明 yield from 的生成器沒有執(zhí)行完成,這個(gè)時(shí)候虛擬機(jī)會(huì)將當(dāng)前棧幀的字節(jié)碼執(zhí)行位置往前移動(dòng),這么做的目的是讓下一次生成器執(zhí)行的時(shí)候繼續(xù)執(zhí)行 YIELD_FROM 字節(jié)碼,這就是 YIELD_FROM 能夠?qū)⒘硪粋€(gè)生成器對(duì)象執(zhí)行完整的秘密。
總結(jié)
在本篇文章當(dāng)中主要分析的生成器內(nèi)部實(shí)現(xiàn)原理和相關(guān)的兩個(gè)重要的字節(jié)碼,分析了生成器能夠停下來還能夠恢復(fù)執(zhí)行的原因。本文最重要的兩點(diǎn)就是區(qū)分函數(shù)和生成器和 YIELD 、YIELD_FROM 兩個(gè)字節(jié)碼,生成器是生成器函數(shù)返回的對(duì)象,YIELD 會(huì)直接進(jìn)行函數(shù)返回,虛擬機(jī)不會(huì)繼續(xù)往下執(zhí)行,YIELD_FROM 除了會(huì)進(jìn)行函數(shù)返回還會(huì)將字節(jié)碼的執(zhí)行位置往前移動(dòng),以保證 YIELD_FROM 下一次還能夠被執(zhí)行。
本篇文章是深入理解 python 虛擬機(jī)系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
更多精彩內(nèi)容合集可訪問項(xiàng)目:https://github.com/Chang-LeHung/CSCore
以上就是深入理解python虛擬機(jī)生成器停止背后原理的詳細(xì)內(nèi)容,更多關(guān)于python虛擬機(jī)生成器停止原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python實(shí)現(xiàn)word文檔內(nèi)容智能提取以及合成
這篇文章主要為大家詳細(xì)介紹了如何使用Python實(shí)現(xiàn)從10個(gè)左右的docx文檔中抽取內(nèi)容,再調(diào)整語言風(fēng)格后生成新的文檔,感興趣的小伙伴可以了解一下2025-04-04使用PySpider進(jìn)行IP代理爬蟲的技巧與實(shí)踐分享
PySpider是一個(gè)基于Python的強(qiáng)大的開源網(wǎng)絡(luò)爬蟲框架,它使用簡(jiǎn)單、靈活,并且具有良好的擴(kuò)展性,本文將介紹如何使用PySpider進(jìn)行IP代理爬蟲,并提供一些技巧和實(shí)踐經(jīng)驗(yàn),文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下2024-03-03Python?內(nèi)置logging?使用詳細(xì)介紹
提供日志記錄的接口和眾多處理模塊,供用戶存儲(chǔ)各種格式的日志,幫助調(diào)試程序或者記錄程序運(yùn)行過程中的輸出信息,這篇文章主要介紹了Python?內(nèi)置logging?使用講解,需要的朋友可以參考下2022-07-07python讀寫json文件的簡(jiǎn)單實(shí)現(xiàn)
這篇文章主要介紹了python讀寫json文件的簡(jiǎn)單實(shí)現(xiàn),實(shí)例分析了各種讀寫json的方法和技巧,有興趣的可以了解一下2017-04-04基于keras 模型、結(jié)構(gòu)、權(quán)重保存的實(shí)現(xiàn)
今天小編就為大家分享一篇基于keras 模型、結(jié)構(gòu)、權(quán)重保存的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-01-01python使用scapy掃描內(nèi)網(wǎng)IP或端口的方法實(shí)現(xiàn)
Scapy是一個(gè)Python程序,使用戶能夠發(fā)送,嗅探和剖析并偽造網(wǎng)絡(luò)數(shù)據(jù)包,本文主要介紹了python使用scapy掃描內(nèi)網(wǎng)IP或端口的方法實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10