欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Python虛擬機(jī)棧幀對象及獲取源碼學(xué)習(xí)

 更新時(shí)間:2023年03月23日 17:05:08   作者:Blanker  
這篇文章主要為大家介紹了Python虛擬機(jī)棧幀對象及獲取源碼學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

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)文章

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

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

    這篇文章主要給大家介紹了關(guān)于pandas?dataframe獲取所有行名稱與列名稱的相關(guān)資料,Pandas是Python中用于數(shù)據(jù)分析的非常重要的庫,它提供了多種方法來獲取列名,需要的朋友可以參考下
    2023-09-09
  • 基于進(jìn)程內(nèi)通訊的python聊天室實(shí)現(xiàn)方法

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

    這篇文章主要介紹了基于進(jìn)程內(nèi)通訊的python聊天室實(shí)現(xiàn)方法,實(shí)例分析了Python聊天室的相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下
    2015-06-06
  • Jupyter notebook 輸出部分顯示不全的解決方案

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

    這篇文章主要介紹了Jupyter notebook 輸出部分顯示不全的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Python 如何求矩陣的逆

    Python 如何求矩陣的逆

    這篇文章主要介紹了Python 如何求矩陣的逆案例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-03-03
  • 使用numpy nonzero 找出非0元素

    使用numpy nonzero 找出非0元素

    這篇文章主要介紹了使用numpy nonzero 找出非0元素的方法,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-05-05
  • python本地文件服務(wù)器實(shí)例教程

    python本地文件服務(wù)器實(shí)例教程

    這篇文章主要給大家介紹了關(guān)于python本地文件服務(wù)器的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-05-05
  • 最新評論