深入理解python虛擬機生成器停止背后原理
深入理解 python 虛擬機:生成器停止背后的魔法
在本篇文章當中主要給大家介紹 Python 當中生成器的實現(xiàn)原理,尤其是生成器是如何能夠被停止執(zhí)行,而且還能夠被恢復的,這是一個非常讓人疑惑的地方。因為這與我們通常使用的函數(shù)的直覺是相違背的,函數(shù)之后執(zhí)行完成之后才會返回,而生成表面是函數(shù)的形式,但是這違背了我們正常的編程直覺。
深入理解生成器與函數(shù)的區(qū)別
為了從根本上建立對生成器的認識,我們首先就需要深入理解一下生成器和函數(shù)的區(qū)別。其實在從虛擬機的層面來看,他們兩個都是對象,只不過一個是生成器對象,一個是函數(shù)對象。在 Python 當中,如果你在函數(shù)里面使用了 yield 語句,那么你的這個函數(shù)在被調(diào)用的時候就不會被執(zhí)行,而是會返回一個生成器對象。
>>> 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 當中有的對象是可以直接調(diào)用的,比如你自己的類如果實現(xiàn)了__call__方法的話,這個類生成的對象就是一個可調(diào)用對象,在 Python 當中一個最常見的可調(diào)用對象就是函數(shù)了,生成器和函數(shù)的區(qū)別之一就是,生成器不能夠直接被調(diào)用,而函數(shù)可以。
>>> generator() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'generator' object is not callable >>>
在上面的代碼當中我們要明確 bar 是一個函數(shù),但是這個函數(shù)和正常的函數(shù)有一點區(qū)別,這個函數(shù)在被調(diào)用的時候不會直接執(zhí)行代碼,而是會返回一個生成器對象,因為在這個函數(shù)體當中使用了 yield 語句,我們稱這種函數(shù)為生成器函數(shù) (generator function),在 Python 當中你可以通過查看一個函數(shù)的 co_flags 字段查看一個函數(shù)的屬性,如果這個字段和 0x0020 進行 & 操作之后的結(jié)果大于 0,那么就說明這個函數(shù)是一個生成器函數(shù)。
>>> (bar.__code__.co_flags & 0x0020) > 0 True >>> bar.__code__.co_flags & 0x0020 32
從上面的代碼當中我們可以看到 bar 就是一個生成器函數(shù),除了上面的方法 Python 的標準庫也提供了方法去輔助我們進行判斷。
>>> import inspect >>> inspect.isgeneratorfunction(bar) True
上面的特性在 Python 程序進行編譯的時候,編譯器可以做到這一點,當發(fā)現(xiàn)一個函數(shù)當中存在類似 yield 的語句的時候就在函數(shù)的 co_flags 字段當中和 0x0020 進行或操作,然后將這個值保存在 co_flags 當中。
總之生成器和函數(shù)之間的關系為:生成器對象是通過調(diào)用生成器函數(shù)得到的,調(diào)用生成器函數(shù)的返回對象是生成器。
虛實交錯的時空魔法
首先我們需要了解的是,如果我們想讓一個生成器對象執(zhí)行下去的話,我們可以使用 next 或者 send 函數(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 實現(xiàn)的虛擬機當中,如果我們想要正確的使用 send 函數(shù)首先需要讓生成器對象執(zhí)行到第一個 yield 語句,我們可以使用 next(generator) 或者 generator.send(None)。比如在上面的第一條語句當中執(zhí)行 next(generator),運行到語句 res = yield 1,但是這條語句還沒有執(zhí)行完,需要我們調(diào)用 send 函數(shù)之后才能夠完成賦值操作,send 函數(shù)的參數(shù)會被賦值給變量 res 。當整個函數(shù)體執(zhí)行完成之后虛擬機就會拋出 StopIteration 異常,并且將返回值保存到 StopIteration 異常對象當中:
>>> 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'
>>>上面的代碼當中可以看到,我們正確的執(zhí)行力我們在上面談到的生成器的使用方法,并且將生成器執(zhí)行完成之后的返回值保存到異常的 value 當中。
生成器內(nèi)部實現(xiàn)原理
從上面的關于生成器的使用方式來看,生成器可以在函數(shù)執(zhí)行到一半的時候停止,然后繼續(xù)恢復執(zhí)行,為了實現(xiàn)這一點我們就需要有一種手段去保存函數(shù)執(zhí)行的狀態(tài)。但是我們需要保存函數(shù)執(zhí)行的那些狀態(tài)呢?最重要的兩點就是代碼現(xiàn)在執(zhí)行到什么位置了,因為我們之后要繼續(xù)從下一條指令開始恢復執(zhí)行,同時我們需要保存虛擬機的??臻g,就是在執(zhí)行字節(jié)碼的時候使用到的 valuestack,注意這不是棧幀,同時還有執(zhí)行函數(shù)的局部變量表,這里主要是保存一些局部變量的。而這些東西都保存在虛擬機的棧幀當中了,這一點我們在前面的文章當中已經(jīng)詳細介紹過了。
因此根據(jù)這些分析我們應該知道了,生成器里面最重要的就是一個虛擬機的棧幀數(shù)據(jù)結(jié)構(gòu)了。一個生成器對象當中一定需要有一個虛擬機的棧幀,在 CPython 的實現(xiàn)當中,生成器對象的數(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: 這個字段就是表示生成器所擁有的棧幀。
- gi_running: 表示生成器是否在運行。
- gi_code: 表示對應生成器函數(shù)的代碼(字節(jié)碼)。
- gi_weakreflist: 用于保存這個棧幀對象保存的弱引用對象。
- gi_name 和 gi_qualname 都是表示生成器的名字,后者更加詳細。
- gi_exc_state: 用于保存執(zhí)行生成器代碼之前的程序狀態(tài),因為之前的代碼可能已經(jīng)產(chǎn)生一些異常了,這個主要用于保存之前的程序狀態(tài),待生成器返回之后就進行恢復。
class A:
def hello(self):
yield 1
if __name__ == '__main__':
g = A().hello()
print(g.__name__)
print(g.__qualname__)上面的程序輸出結(jié)果為:
hello
A.hello
生成器對應的字節(jié)碼行為
我們通過下面的例子來分析一下,生成器 yield 對應的字節(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在上面的程序當中只有和生成器相關的字節(jié)碼為 YIELD_VALUE,在加載完常量 1 之后就會執(zhí)行 YIELD_VALUE 指令,虛擬機在執(zhí)行完 yield 指令之后,就會直接返回,此時虛擬機的狀態(tài)——valuestack 和當前指令執(zhí)行的位置(在上面的這個例子當中就是 4)都會被保存到虛擬機棧幀當中,當下一次執(zhí)行生成器的代碼的時候就會直接從 POP_TOP 指令直接執(zhí)行。
我們再來看一下另外一個比較重要的指令 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)在用一個簡單的例子重新回顧一下程序的行為:
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 的兩個值都會被返回,這些魔法隱藏在字節(jié)碼 YIELD_FROM 當中。YIELD_FROM 字節(jié)碼會調(diào)用棧頂上的生成器對象的 send 方法,并且將參數(shù)生成器對象 gen 的返回結(jié)果返回,比如 1 和 2 這兩個值會被返回到 generator_b ,然后 generator_b 會將這個結(jié)果繼續(xù)傳播出來。
- 在這個字節(jié)碼執(zhí)行最后會進行判斷虛擬機當中是否出現(xiàn)了 StopIteration 異常,如果出現(xiàn)了則說 yield from 的生成器已經(jīng)執(zhí)行完了,則 generator_b 繼續(xù)往下執(zhí)行。
- 如果沒有 StopIteration 異常,則說明 yield from 的生成器沒有執(zhí)行完成,這個時候虛擬機會將當前棧幀的字節(jié)碼執(zhí)行位置往前移動,這么做的目的是讓下一次生成器執(zhí)行的時候繼續(xù)執(zhí)行 YIELD_FROM 字節(jié)碼,這就是 YIELD_FROM 能夠?qū)⒘硪粋€生成器對象執(zhí)行完整的秘密。
總結(jié)
在本篇文章當中主要分析的生成器內(nèi)部實現(xiàn)原理和相關的兩個重要的字節(jié)碼,分析了生成器能夠停下來還能夠恢復執(zhí)行的原因。本文最重要的兩點就是區(qū)分函數(shù)和生成器和 YIELD 、YIELD_FROM 兩個字節(jié)碼,生成器是生成器函數(shù)返回的對象,YIELD 會直接進行函數(shù)返回,虛擬機不會繼續(xù)往下執(zhí)行,YIELD_FROM 除了會進行函數(shù)返回還會將字節(jié)碼的執(zhí)行位置往前移動,以保證 YIELD_FROM 下一次還能夠被執(zhí)行。
本篇文章是深入理解 python 虛擬機系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
更多精彩內(nèi)容合集可訪問項目:https://github.com/Chang-LeHung/CSCore
以上就是深入理解python虛擬機生成器停止背后原理的詳細內(nèi)容,更多關于python虛擬機生成器停止原理的資料請關注腳本之家其它相關文章!
相關文章
Python?調(diào)用函數(shù)時檢查參數(shù)的類型是否合規(guī)的實現(xiàn)代碼
這篇文章主要介紹了Python?調(diào)用函數(shù)時檢查參數(shù)的類型是否合規(guī)的實現(xiàn)代碼,本文給大家講解的非常詳細,需要的朋友可以參考下2024-06-06
基于Python實現(xiàn)浪漫的彈窗祝福滿屏飄字效果(附完整代碼)
這篇文章主要為大家詳細介紹了如何基于Python實現(xiàn)浪漫的彈窗祝福滿屏飄字效果,文中的示例代碼講解詳細,具有一定的借鑒價值,感興趣的小伙伴可以了解下2026-01-01
Python實戰(zhàn)小項目之Mnist手寫數(shù)字識別
MNIST 數(shù)據(jù)集已經(jīng)是一個被”嚼爛”了的數(shù)據(jù)集, 很多教程都會對它”下手”, 幾乎成為一個 “典范”. 不過有些人可能對它還不是很了解, 下面通過一個小實例來帶你了解它2021-10-10
通過pycharm的database設置進行數(shù)據(jù)庫的可視化方式
這篇文章主要介紹了通過pycharm的database設置進行數(shù)據(jù)庫的可視化方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09
閉包在python中的應用之translate和maketrans用法詳解
這篇文章主要介紹了閉包在python中的應用之translate和maketrans用法,是比較實用的技巧,需要的朋友可以參考下2014-08-08

