Python虛擬機(jī)棧幀對象及獲取源碼學(xué)習(xí)
Python虛擬機(jī)
注:本篇是根據(jù)教程學(xué)習(xí)記錄的筆記,部分內(nèi)容與教程是相同的,因?yàn)檗D(zhuǎn)載需要填鏈接,但是沒有,所以填的原創(chuàng),如果侵權(quán)會(huì)直接刪除。此外,本篇內(nèi)容大部分都咨詢了ChatGPT,為筆者解決了很多問題。
問題:
在 Python 程序執(zhí)行過程與字節(jié)碼中,我們研究了Python程序的編譯過程:通過Python解釋器中的編譯器對 Python 源碼進(jìn)行編譯,最終獲得代碼對象 PyCodeObject 。編譯器根據(jù)語法規(guī)則對源碼進(jìn)行作用域的劃分,并以此為單位來編譯源碼,最終為每個(gè)作用域生成一個(gè)代碼對象。代碼對象則保存了字節(jié)碼,以及相關(guān)名字、常量等靜態(tài)上下文信息。
(上面這段話是原文章的作者總結(jié)的,我個(gè)人覺得還是很到位的,大家也可以再回顧一下這篇筆記的內(nèi)容: Python 程序執(zhí)行過程與字節(jié)碼,更深刻體會(huì)下。)
那么當(dāng)我們得到了編譯產(chǎn)出的代碼對象后,虛擬機(jī)是如何解析并執(zhí)行其中的字節(jié)碼指令的呢?與語法作用域相對應(yīng)的運(yùn)行時(shí)名字空間,在虛擬機(jī)中又是如何動(dòng)態(tài)維護(hù)的呢?
1. 棧幀對象
1.1 PyFrameObject
- 當(dāng) Python 解釋器加載一個(gè)模塊或者執(zhí)行函數(shù)時(shí),會(huì)為對應(yīng)的 PyCodeObject 創(chuàng)建一個(gè) PyFrameObject 對象,并將其壓入 Python 解釋器的執(zhí)行棧中。以函數(shù)為例,PyFrameObject 對象表示函數(shù)調(diào)用的棧幀對象,它包含了函數(shù)調(diào)用時(shí)的所有狀態(tài)信息,包括局部變量、棧、當(dāng)前指令等信息。
具體地我們來看一下執(zhí)行上下文的具體結(jié)構(gòu)——PyFrameObject,源碼如下:
typedef struct _frame { PyObject_VAR_HEAD struct _frame *f_back; /* previous frame, or NULL */ PyCodeObject *f_code; /* code segment */ PyObject *f_builtins; /* builtin symbol table (PyDictObject) */ PyObject *f_globals; /* global symbol table (PyDictObject) */ PyObject *f_locals; /* local symbol table (any mapping) */ PyObject **f_valuestack; /* points after the last local */ /* Next free slot in f_valuestack. Frame creation sets to f_valuestack. Frame evaluation usually NULLs it, but a frame that yields sets it to the current stack top. */ PyObject **f_stacktop; PyObject *f_trace; /* Trace function */ char f_trace_lines; /* Emit per-line trace events? */ char f_trace_opcodes; /* Emit per-opcode trace events? */ /* Borrowed reference to a generator, or NULL */ PyObject *f_gen; int f_lasti; /* Last instruction if called */ /* Call PyFrame_GetLineNumber() instead of reading this field directly. As of 2.3 f_lineno is only valid when tracing is active (i.e. when f_trace is set). At other times we use PyCode_Addr2Line to calculate the line from the current bytecode index. */ int f_lineno; /* Current line number */ int f_iblock; /* index in f_blockstack */ char f_executing; /* whether the frame is still executing */ PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */ PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */ } PyFrameObject;
源碼分析(只列出重要字段):
思考:PyFrameObject為什么沒有記錄閉包信息?
- f_back:表示當(dāng)前棧幀的前一個(gè)棧幀,即調(diào)用當(dāng)前函數(shù)的函數(shù)的棧幀。Python解釋器使用這個(gè)字段來實(shí)現(xiàn)函數(shù)調(diào)用的遞歸和返回。如果當(dāng)前函數(shù)是最外層函數(shù),即沒有調(diào)用它的函數(shù),則該字段為NULL。
- f_code:表示當(dāng)前棧幀對應(yīng)的 PyCodeObject 對象,即當(dāng)前函數(shù)的字節(jié)碼和相關(guān)信息。Python 解釋器使用這個(gè)字段來執(zhí)行函數(shù)中的字節(jié)碼指令。
- f_builtins:表示當(dāng)前棧幀的內(nèi)建變量字典,即當(dāng)前函數(shù)中訪問的所有內(nèi)建函數(shù)和對象的名稱和值。Python 解釋器使用這個(gè)字段來實(shí)現(xiàn)對內(nèi)建函數(shù)和對象的訪問。
- f_locals:表示當(dāng)前棧幀的局部變量字典,即當(dāng)前函數(shù)的所有局部變量的名稱和值。Python 解釋器使用這個(gè)字段來實(shí)現(xiàn)變量的讀取和寫入操作。
- f_lasti:表示當(dāng)前棧幀執(zhí)行的最后一條指令的指令碼在字節(jié)碼序列中的索引。Python 解釋器使用這個(gè)字段來記錄當(dāng)前函數(shù)執(zhí)行的進(jìn)度,以便在函數(shù)被中斷或者函數(shù)返回時(shí),能夠恢復(fù)到正確的執(zhí)行位置。
- f_lineno:表示當(dāng)前棧幀執(zhí)行的源代碼行號(hào)。Python 解釋器使用這個(gè)字段來跟蹤當(dāng)前函數(shù)的行號(hào),以便在發(fā)生異常時(shí)能夠提供更準(zhǔn)確的錯(cuò)誤信息。
- f_localsplus:表示當(dāng)前棧幀的棧頂指針,即當(dāng)前函數(shù)調(diào)用的棧的頂部。Python 解釋器使用這個(gè)字段來實(shí)現(xiàn)函數(shù)調(diào)用的參數(shù)傳遞和返回值傳遞。
- PyFrameObject 對象本身不記錄閉包相關(guān)的信息是出于設(shè)計(jì)上的考慮。一個(gè)主要的原因是為了保持執(zhí)行棧的簡潔性和高效性。
- 閉包是一種在 Python 中廣泛使用的編程模式,但是它在實(shí)現(xiàn)上是比較復(fù)雜的。在解釋器執(zhí)行 Python 代碼時(shí),一個(gè)函數(shù)在定義時(shí)可能沒有引用外部變量,但是在運(yùn)行時(shí)卻可能引用了。因此,如果要記錄函數(shù)中使用的外部變量,就需要在運(yùn)行時(shí)動(dòng)態(tài)地創(chuàng)建一個(gè)閉包對象,并將其與函數(shù)對象關(guān)聯(lián)起來。這就會(huì)給執(zhí)行棧的實(shí)現(xiàn)帶來很大的復(fù)雜性。
- 另一個(gè)原因是,閉包可能會(huì)被頻繁地創(chuàng)建和銷毀,而在執(zhí)行棧中保存大量的閉包信息會(huì)導(dǎo)致執(zhí)行效率變慢,甚至可能引起內(nèi)存泄漏。因此,Python 解釋器在設(shè)計(jì)執(zhí)行棧時(shí),選擇不記錄閉包相關(guān)的信息,以保持執(zhí)行棧的簡潔性和高效性。
- 雖然 PyFrameObject 對象本身不記錄閉包相關(guān)的信息,但是 Python 解釋器可以通過其他方式來獲取函數(shù)的閉包信息,例如通過函數(shù)對象的 closure 屬性。
PyFrameObject結(jié)構(gòu)圖如下:
- 其中,f_code字段保存了當(dāng)前執(zhí)行的代碼對象,最核心的字節(jié)碼就在代碼對象中。而f_lasti字段則保存著上條已執(zhí)行字節(jié)碼的編號(hào)。虛擬機(jī)內(nèi)部用一個(gè)C局部變量next_instr維護(hù)下條字節(jié)碼的位置,并據(jù)此加載下一條待執(zhí)行的字節(jié)碼指令,原理和CPU的指令指針寄存器(%rip)一樣。
- 另外,注意到f_back字段執(zhí)行前一個(gè)棧幀對象,也就是調(diào)用者的棧幀對象。這樣一來,棧幀對象按照調(diào)用關(guān)系串成一個(gè)調(diào)用鏈。(這里和x86CPU棧幀布局是如出一轍的,原作者在這里介紹了x86CPU棧幀布局與函數(shù)調(diào)用之間的關(guān)系,筆者能力有限就不介紹了,大家感興趣的可以自行查找相關(guān)資料(主要還是微機(jī)原理和匯編學(xué)的不是很好。。。))
1.2 棧幀對象鏈
現(xiàn)在,我們以具體例子來考察Python棧幀對象鏈以及函數(shù)調(diào)用之間的關(guān)系:
pi = 3.14 def square(r): return r ** 2 def circle_area(r): return pi * square(r) def main(): print(circle_area(5)) if __name__ == '__main__': main()
當(dāng)Python開始執(zhí)行這個(gè)程序時(shí),虛擬機(jī)先創(chuàng)建一個(gè)棧幀對象,用于執(zhí)行模塊代碼對象:
當(dāng)虛擬機(jī)執(zhí)行到模塊代碼第13行時(shí),發(fā)生了函數(shù)調(diào)用。這時(shí),虛擬機(jī)會(huì)新建一個(gè)棧幀對象,并開始執(zhí)行函數(shù)main()的代碼對象:
隨著函數(shù)調(diào)用逐層深入,當(dāng)調(diào)用square()函數(shù)時(shí),調(diào)用鏈達(dá)到最長:
當(dāng)函數(shù)調(diào)用完畢后,虛擬機(jī)通過f_back字段找到前一個(gè)棧幀對象并回到調(diào)用者代碼中繼續(xù)執(zhí)行。
1.3 棧幀獲取
棧幀對象PyFrameObject中保存著Python運(yùn)行時(shí)信息,在底層執(zhí)行流控制以及程序調(diào)試中非常有用。在Python代碼層面,我們可以通過sys模塊中的_getframe()函數(shù),即可獲得當(dāng)前棧幀對象:
>>> import sys >>> frame = sys._getframe() >>> frame <frame at 0x00000183FA78F870, file '<pyshell#1>', line 1, code <module>> >>> dir(frame) ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'f_back', 'f_builtins', 'f_code', 'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_trace', 'f_trace_lines', 'f_trace_opcodes']
拿到棧幀對象之后,我們來具體看一下相關(guān)的屬性值,以之前的求面積的函數(shù)為例:
>>> import sys >>> pi = 3.14 >>> def square(r): frame = sys._getframe() while frame: print('name:', frame.f_code.co_name) print('Locals', list(frame.f_locals.keys())) print('Globals', list(frame.f_globals.keys())) print('===========') frame = frame.f_back return r ** 2 >>> def circle_area(r): return pi * square(r) >>> def main(): print(circle_area(2)) >>> if __name__ == '__main__': main() name: square Locals ['r', 'frame'] Globals ['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'sys', 'pi', 'square', 'circle_area', 'main'] =========== name: circle_area Locals ['r'] Globals ['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'sys', 'pi', 'square', 'circle_area', 'main'] =========== name: main Locals [] Globals ['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'sys', 'pi', 'square', 'circle_area', 'main'] =========== name: <module> Locals ['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'sys', 'pi', 'square', 'circle_area', 'main'] Globals ['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'sys', 'pi', 'square', 'circle_area', 'main'] =========== 12.56
小拓展:自定義函數(shù)實(shí)現(xiàn)sys._getframe()功能:(這里是原作者舉的一個(gè)例子,個(gè)人感覺對相關(guān)知識(shí)的理解是有幫助的)
當(dāng)Python程序拋出異常時(shí),會(huì)將執(zhí)行上下文帶出來,保存在異常中:
>>> try: 1 / 0 except Exception as e: print(e.__traceback__.tb_frame) <frame at 0x000002440D95BC50, file '<pyshell#5>', line 4, code <module>>
因此,我們可以自定義一個(gè)getframe()函數(shù):
>>> def getframe(): try: 1 / 0 except Exception as e: return e.__traceback__.tb_frame.f_back
注意:getframe()中通過異常獲得的是自己的棧幀對象e.traceback.tb_frame,所以還需要通過f_back字段找到調(diào)用者的棧幀。
2. 字節(jié)碼執(zhí)行
Python 虛擬機(jī)執(zhí)行代碼對象的主要函數(shù)有兩個(gè):
PyEval_EvalCodeEx() 是通用接口,一般用于函數(shù)這樣帶參數(shù)的執(zhí)行場景:
PyObject * PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals, PyObject *const *args, int argcount, PyObject *const *kws, int kwcount, PyObject *const *defs, int defcount, PyObject *kwdefs, PyObject *closure);
PyEval_EvalCode() 是更高層封裝,用于模塊等無參數(shù)的執(zhí)行場景:
PyObject * PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals);
這兩個(gè)函數(shù)最終調(diào)用 _PyEval_EvalCodeWithName() 函數(shù),初始化棧幀對象并調(diào)用 PyEval_EvalFrame 系列函數(shù)進(jìn)行處理。棧幀對象將貫穿代碼對象執(zhí)行的始終,負(fù)責(zé)維護(hù)執(zhí)行時(shí)所需的一切上下文信息。而PyEval_EvalFrame 系列函數(shù)最終調(diào)用 _PyEval_EvalFrameDefault() 函數(shù),虛擬機(jī)執(zhí)行的核心就在這里(具體源碼這里就不講解了)。
PyObject * PyEval_EvalFrame(PyFrameObject *f); PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag); PyObject* _Py_HOT_FUNCTION _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag);
文章后續(xù)以順序執(zhí)行、if判斷、while循環(huán)詳細(xì)講解了字節(jié)碼的執(zhí)行過程,這里筆者就不贅述了。
以上就是Python虛擬機(jī)棧幀對象及獲取源碼學(xué)習(xí)的詳細(xì)內(nèi)容,更多關(guān)于Python虛擬機(jī)棧幀對象獲取的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python matplotlib餅狀圖參數(shù)及用法解析
這篇文章主要介紹了python matplotlib餅狀圖參數(shù)及用法解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11

pandas?dataframe獲取所有行名稱與列名稱方法示例

基于進(jìn)程內(nèi)通訊的python聊天室實(shí)現(xiàn)方法

Jupyter notebook 輸出部分顯示不全的解決方案