午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

深度解密 Python 虛擬機(jī)的執(zhí)行環(huán)境:棧幀對象

 古明地覺O_o 2024-09-30 發(fā)布于北京

楔子

從現(xiàn)在開始,我們將剖析虛擬機(jī)運(yùn)行字節(jié)碼的原理。前面說了,Python 解釋器可以分為兩部分:Python 編譯器和 Python 虛擬機(jī)。

編譯器將源代碼編譯成 PyCodeObject 對象之后,就由虛擬機(jī)接手整個(gè)工作。虛擬機(jī)會(huì)從 PyCodeObject 中讀取字節(jié)碼,并在當(dāng)前的上下文中執(zhí)行,直到所有的字節(jié)碼都被執(zhí)行完畢。

那么問題來了,既然源代碼在經(jīng)過編譯之后,字節(jié)碼指令以及靜態(tài)信息都存儲(chǔ)在 PyCodeObject 當(dāng)中,那么是不是意味著虛擬機(jī)就在 PyCodeObject 對象上進(jìn)行所有的動(dòng)作呢?

很明顯不是的,因?yàn)楸M管 PyCodeObject 包含了關(guān)鍵的字節(jié)碼指令以及靜態(tài)信息,但有一個(gè)東西是沒有包含、也不可能包含的,就是程序在運(yùn)行時(shí)的執(zhí)行環(huán)境,這個(gè)執(zhí)行環(huán)境在 Python 里面就是棧幀。


棧幀:虛擬機(jī)的執(zhí)行環(huán)境

那什么是棧幀呢?我們舉個(gè)例子。

name = "古明地覺"

def some_func():
    name = "八意永琳"
    print(name)

some_func()
print(name)

上面的代碼當(dāng)中出現(xiàn)了兩個(gè) print(name),它們的字節(jié)碼指令相同,但執(zhí)行的效果卻顯然是不同的,這樣的結(jié)果正是執(zhí)行環(huán)境的不同所產(chǎn)生的。因?yàn)榄h(huán)境的不同,name 的值也不同。

因此同一個(gè)符號(hào)在不同環(huán)境中可能指向不同的類型、不同的值,必須在運(yùn)行時(shí)進(jìn)行動(dòng)態(tài)捕捉和維護(hù),這些信息不可能在 PyCodeObject 對象中被靜態(tài)存儲(chǔ)。

因此虛擬機(jī)并不是在 PyCodeObject 對象上執(zhí)行操作的,而是在棧幀對象上。虛擬機(jī)在執(zhí)行時(shí),會(huì)根據(jù) PyCodeObject 對象動(dòng)態(tài)創(chuàng)建出棧幀對象,然后在棧幀里面執(zhí)行字節(jié)碼。所以棧幀是虛擬機(jī)執(zhí)行的上下文,執(zhí)行時(shí)依賴的所有信息都存儲(chǔ)在棧幀中。

因此對于上面的代碼,我們可以大致描述一下流程:

  • 首先基于模塊的 PyCodeObject 創(chuàng)建一個(gè)棧幀,假設(shè)叫 A,所有的字節(jié)碼都會(huì)在棧幀中執(zhí)行,虛擬機(jī)可以從棧幀里面獲取變量的值,也可以修改;

  • 當(dāng)發(fā)生函數(shù)調(diào)用的時(shí)候,這里是 some_func,那么虛擬機(jī)會(huì)在棧幀 A 之上,為 some_func 創(chuàng)建一個(gè)新的棧幀,假設(shè)叫 B,然后在棧幀 B 里面執(zhí)行函數(shù) some_func 的字節(jié)碼指令;

  • 在棧幀 B 里面也有一個(gè)名字為 name 的變量,但由于執(zhí)行環(huán)境、或者說棧幀的不同,name 指向的對象也不同;

  • 一旦函數(shù) some_func 的字節(jié)碼指令全部執(zhí)行完畢,那么會(huì)將當(dāng)前的棧幀 B 銷毀(也可以保留),再回到調(diào)用者的棧幀中來。就像是遞歸一樣,每當(dāng)調(diào)用函數(shù)時(shí),就會(huì)在當(dāng)前棧幀之上創(chuàng)建一個(gè)新的棧幀,一層一層創(chuàng)建,一層一層返回;


虛擬機(jī)和操作系統(tǒng)

不難發(fā)現(xiàn),Python 虛擬機(jī)執(zhí)行字節(jié)碼這個(gè)過程,就是在模擬操作系統(tǒng)運(yùn)行可執(zhí)行文件。比如:

程序加載

  • 操作系統(tǒng):加載可執(zhí)行文件到內(nèi)存,設(shè)置程序計(jì)數(shù)器。

  • Python 虛擬機(jī):加載 .pyc 文件中的 PyCodeObject 對象,初始化字節(jié)碼指令指針。

內(nèi)存管理

  • 操作系統(tǒng):為進(jìn)程分配內(nèi)存空間,管理堆和棧。

  • Python 虛擬機(jī):創(chuàng)建和管理 Python 對象,處理內(nèi)存分配和垃圾回收。

指令執(zhí)行

  • 操作系統(tǒng):CPU 逐條執(zhí)行機(jī)器指令。

  • Python 虛擬機(jī):虛擬機(jī)逐條執(zhí)行字節(jié)碼指令。

資源管理

  • 操作系統(tǒng):管理文件句柄、網(wǎng)絡(luò)連接等系統(tǒng)資源。

  • Python 虛擬機(jī):管理文件對象、套接字等 Python 級(jí)別的資源。

異常處理

  • 操作系統(tǒng):處理硬件中斷和軟件異常。

  • Python 虛擬機(jī):捕獲和處理 Python 異常。

我們簡單地畫一張示意圖,來看看在一臺(tái)普通的 x64 機(jī)器上,可執(zhí)行文件是以什么方式運(yùn)行的,在這里主要關(guān)注棧幀的變化。假設(shè)有三個(gè)函數(shù),函數(shù) f 調(diào)用了函數(shù) g,函數(shù) g 又調(diào)用了函數(shù) h。

首先 CPU 有兩個(gè)關(guān)鍵的寄存器,它們在函數(shù)調(diào)用和棧幀管理中扮演關(guān)鍵角色。

RSP(Stack Pointer):棧指針,指向當(dāng)前棧幀的頂部,或者說最后一個(gè)入棧的元素。因此隨著元素的入棧和出棧,RSP 會(huì)動(dòng)態(tài)變化。由于地址從棧底到棧頂是逐漸減小的,所以 RSP 會(huì)隨著數(shù)據(jù)入棧而減小,隨著數(shù)據(jù)出棧而增大。當(dāng)然不管 RSP 怎么變,它始終指向當(dāng)前棧的頂部。

RBP(Base Pointer):基指針,指向當(dāng)前棧幀的基址,它的作用是提供一個(gè)固定的參考點(diǎn),用于訪問當(dāng)前函數(shù)的局部變量和參數(shù)。當(dāng)新的幀被創(chuàng)建時(shí),它的基址會(huì)保存上一個(gè)幀的基址,并由 RBP 指向。

我們用一段 C 代碼來解釋一下。

#include <stdio.h>

int add(int a, int b) {
    int c = a + b;
    return c;
}

int main() {
    int a = 11;
    int b = 22;
    int result = add(a, b);
    printf("a + b = %d\n", result);
}

當(dāng)執(zhí)行函數(shù) add 時(shí),那么當(dāng)前幀顯然就是函數(shù) add 的棧幀,而調(diào)用者的幀(上一級(jí)棧幀)顯然就是函數(shù) main 的棧幀。

棧是先入后出的數(shù)據(jù)結(jié)構(gòu),地址從棧底到棧頂是減小的。對于一個(gè)函數(shù)而言,所有對局部變量的操作都在自己的棧幀中完成,而調(diào)用函數(shù)的時(shí)候則會(huì)為其創(chuàng)建新的棧幀。

當(dāng)執(zhí)行函數(shù) main 的時(shí)候,RSP 指向 main 棧幀的頂部,RBP 指向 main 棧幀的基址。然后在 main 里面又調(diào)用了函數(shù) add,那么毫無疑問,系統(tǒng)會(huì)在地址空間中,在 main 的棧幀之上為 add 創(chuàng)建棧幀。然后讓 RSP 指向 add 棧幀的頂部,RBP 指向 add 棧幀的基址,而 add 棧幀的基址保存了上一級(jí)棧幀(main 棧幀)的基址。

當(dāng)函數(shù) add 執(zhí)行結(jié)束時(shí),會(huì)銷毀對應(yīng)棧幀,再將 RSP 和 RBP 恢復(fù)為創(chuàng)建 add 棧幀之前的值,這樣程序的執(zhí)行流程就又回到了函數(shù) main 里面,當(dāng)然程序的運(yùn)行空間也回到了函數(shù) main 的棧幀中。

不難發(fā)現(xiàn),通過兩個(gè) CPU 寄存器 RSP、RBP,以及棧幀中保存的上一級(jí)棧幀的基址,完美地維護(hù)了函數(shù)之間的調(diào)用鏈,這就是可執(zhí)行文件在 x64 機(jī)器上的運(yùn)行原理。

那么 Python 里面的棧幀是怎樣的呢?


棧幀的底層結(jié)構(gòu)

相較于 x64 機(jī)器上看到的那個(gè)簡簡單單的棧幀,Python 的棧幀實(shí)際上包含了更多的信息。注:棧幀也是一個(gè)對象。

// Include/pytypedefs.h
typedef struct _frame PyFrameObject;

// Include/internal/pycore_frame.h
struct _frame {
    PyObject_HEAD
    PyFrameObject *f_back;     
    struct _PyInterpreterFrame *f_frame; 
    PyObject *f_trace;          
    int f_lineno;               
    char f_trace_lines;         
    char f_trace_opcodes;       
    char f_fast_as_locals;      
};

typedef struct _PyInterpreterFrame {
    PyCodeObject *f_code; 
    struct _PyInterpreterFrame *previous;
    PyObject *f_funcobj; 
    PyObject *f_globals; 
    PyObject *f_builtins; 
    PyObject *f_locals;
    PyFrameObject *frame_obj;
    _Py_CODEUNIT *prev_instr;
    int stacktop;
    uint16_t return_offset;
    char owner;
    PyObject *localsplus[1];
} _PyInterpreterFrame;

棧幀在底層由 PyFrameObject 表示,在 3.11 之前,所有字段都保存在該結(jié)構(gòu)體中。但里面有一部分字段,在大部分情況下都用不到,比如一些用于 Debug 的字段。而這些不常用的字段,顯然會(huì)導(dǎo)致內(nèi)存浪費(fèi),因?yàn)閯?chuàng)建棧幀時(shí)要為所有字段都申請內(nèi)存空間。

于是從 3.11 開始,虛擬機(jī)將 PyFrameObject 里面的核心字段提取出來,形成了更加輕量級(jí)的 _PyInterpreterFrame,從而減少內(nèi)存使用并提高性能。

  • _PyInterpreterFrame:棧幀的核心結(jié)構(gòu),這是一個(gè)輕量級(jí)的 C 結(jié)構(gòu),只包含執(zhí)行所需的基本信息,虛擬機(jī)會(huì)在內(nèi)部使用它。

  • PyFrameObject:完整的棧幀對象,在需要更全面的幀信息時(shí)使用。比如從 Python 級(jí)別獲取棧幀時(shí),拿到的對象在底層對應(yīng)的就是 PyFrameObject 結(jié)構(gòu)體。

通過這種拆分,虛擬機(jī)在大多數(shù)情況下只需使用輕量級(jí)的 _PyInterpreterFrame 即可,只有在需要完整的幀信息時(shí),才會(huì)創(chuàng)建 PyFrameObject。

但要強(qiáng)調(diào)的是,由于 _PyInterpreterFrame 里面沒有 PyObject,所以它不是 Python 對象,它只是包含了棧幀的核心結(jié)構(gòu),真正的棧幀對象仍是 PyFrameObject。只不過對于虛擬機(jī)而言,很多時(shí)候只需實(shí)例化 _PyInterpreterFrame 結(jié)構(gòu)體,即可完成任務(wù)。

另外 _PyInterpreterFrame 除了更輕量、結(jié)構(gòu)更緊湊、創(chuàng)建速度快之外,它對 CPU 緩存也非常友好。

我們知道 Python 對象都是申請?jiān)诙焉系模瑮膊焕?,?dāng)調(diào)用嵌套函數(shù)時(shí),這些棧幀對象會(huì)零散在堆區(qū)的不同位置,對緩存不友好。但 _PyInterpreterFrame 則不是這樣,虛擬機(jī)為它專門引入了一個(gè) Stack,這是一段預(yù)分配的內(nèi)存區(qū)域,專門用于存儲(chǔ) _PyInterpreterFrame 實(shí)例。

當(dāng)需要?jiǎng)?chuàng)建 _PyInterpreterFrame 實(shí)例時(shí),只需要改動(dòng)一下棧指針,內(nèi)存便創(chuàng)建好了。當(dāng)需要銷毀時(shí),直接將它從棧的頂端彈出即可,不需要顯式地釋放內(nèi)存。并且由于 _PyInterpreterFrame 都是緊密排列在一起,所以對緩存也更加友好。


字段含義解析與代碼演示

下面來看一下這兩個(gè)結(jié)構(gòu)體里面的字段都表示啥含義,不過在解釋字段含義之前,我們需要先知道如何在 Python 中獲取棧幀對象。

import inspect

def foo():
    # 返回當(dāng)前所在的棧幀
    # 這個(gè)函數(shù)實(shí)際上是調(diào)用了 sys._getframe(1)
    return inspect.currentframe()

frame = foo()
print(frame) 
"""
<frame at 0x100de0fc0, file '.../main.py', line 6, code foo>
"""

print(type(frame)) 
"""
<class 'frame'>
"""

我們看到棧幀的類型是 <class 'frame'>,正如 PyCodeObject 對象的類型是 <class 'code'> 一樣,這兩個(gè)類沒有暴露給我們,所以不可以直接使用。

同理,還有 Python 的函數(shù),類型是 <class 'function'>,模塊的類型是 <class 'module'>。這些解釋器都沒有給我們提供,如果直接使用的話,那么 frame、code、function、module 只是幾個(gè)沒有定義的變量罷了,這些類我們只能通過這種間接的方式獲取。

下面我們來看一下 PyFrameObject 里面每個(gè)字段的含義。

PyObject_HEAD

對象的頭部信息,所以棧幀也是一個(gè)對象。

PyFrameObject *f_back

當(dāng)前棧幀的上一級(jí)棧幀,也就是調(diào)用者的棧幀。所以 x64 機(jī)器是通過 RSP、RBP 兩個(gè)指針維護(hù)函數(shù)的調(diào)用關(guān)系,而 Python 虛擬機(jī)則是通過棧幀的 f_back 字段。

import inspect

def foo():
    return inspect.currentframe()

frame = foo()
print(frame)
"""
<frame at 0x100de0fc0, file '.../main.py', line 6, code foo>
"""

# foo 的上一級(jí)棧幀,顯然對應(yīng)的是模塊的棧幀
print(frame.f_back)
"""
<frame at 0x100adde40, file '.../main.py', line 12, code <module>>
"""

# 相當(dāng)于模塊的上一級(jí)棧幀,顯然是 None
print(frame.f_back.f_back)
"""
None
"""

所以通過棧幀,你可以輕松地獲取完整的函數(shù)調(diào)用鏈路,我們一會(huì)兒演示。

struct _PyInterpreterFrame *f_frame

指向 struct _PyInterpreterFrame 實(shí)例,它包含了棧幀的核心結(jié)構(gòu)。

PyObject *f_trace

追蹤函數(shù),用于調(diào)試。

int f_lineno

獲取該棧幀時(shí)的源代碼行號(hào)。

import inspect

def foo():
    return inspect.currentframe()

frame = foo()
print(frame.f_lineno)  # 4

我們是在第 4 行獲取的棧幀,所以打印結(jié)果是 4。

char f_trace_lines

是否為每一行代碼調(diào)用追蹤函數(shù),當(dāng)設(shè)置為真(非零值)時(shí),每當(dāng)虛擬機(jī)執(zhí)行到一個(gè)新的代碼行時(shí),都會(huì)調(diào)用追蹤函數(shù)。這允許調(diào)試器在每行代碼執(zhí)行時(shí)進(jìn)行干預(yù),比如設(shè)置斷點(diǎn)、檢查變量等。

char f_trace_opcodes

是否為每個(gè)字節(jié)碼指令調(diào)用追蹤函數(shù),當(dāng)設(shè)置為真時(shí),虛擬機(jī)會(huì)在執(zhí)行每個(gè)字節(jié)碼指令之前調(diào)用追蹤函數(shù)。這提供了更細(xì)粒度的控制,允許進(jìn)行指令級(jí)別的調(diào)試。

所以不難發(fā)現(xiàn),f_trace_lines 是行級(jí)追蹤,對應(yīng)源代碼的每一行,通常用于普通的調(diào)試,如設(shè)置斷點(diǎn)、單步執(zhí)行等,并且開銷相對較小。f_trace_opcodes 是指令級(jí)追蹤,對應(yīng)每個(gè)字節(jié)碼指令,通常用于更深層次的調(diào)試,比如分析具體的字節(jié)碼執(zhí)行過程,并且開銷較大。

import sys

def trace_lines(frame, event, arg):
    print(f"行號(hào):{frame.f_lineno},文件名:{frame.f_code.co_filename}")
    return trace_lines

sys.settrace(trace_lines)

設(shè)置追蹤函數(shù)一般需要通過 sys.settrace,不過不常用,了解一下即可。

char f_fast_as_locals

要解釋這個(gè)字段,需要用到后續(xù)的知識(shí),所以這里先簡單了解一下即可。Python 函數(shù)的局部變量是采用數(shù)組存儲(chǔ)的,以便快速訪問,這就是所謂的 fast locals。

但有時(shí)候我們就是需要一個(gè)字典,里面包含所有的局部變量,這時(shí)候可以調(diào)用 locals 函數(shù),將局部變量的名稱和值以 key、value 的形式拷貝到字典中。而 f_fast_as_locals 字段則負(fù)責(zé)標(biāo)記這個(gè)拷貝過程是否發(fā)生過。

然后再來看看 _PyInterpreterFrame 結(jié)構(gòu)體里面的字段,我們說棧幀的核心字段都在該結(jié)構(gòu)體中。

PyCodeObject *f_code

棧幀對象是在 PyCodeObject 之上構(gòu)建的,所以它內(nèi)部一定有一個(gè)字段指向 PyCodeObject。

import inspect

def e():
    f()

def f():
    g()

def g():
    h()

def h():
    frame = inspect.currentframe()  # 獲取棧幀
    func_names = []
    # 只要 frame 不為空,就一直循環(huán),并將函數(shù)名添加到列表中
    while frame is not None:
        func_names.append(frame.f_code.co_name)
        frame = frame.f_back
    print(f"函數(shù)調(diào)用鏈路:{' -> '.join(func_names[:: -1])}")

f()
"""
函數(shù)調(diào)用鏈路:<module> -> f -> g -> h
"""

模塊 -> f -> g -> h,顯然我們獲取了整個(gè)調(diào)用鏈路,是不是很有趣呢?

struct _PyInterpreterFrame *previous

指向上一個(gè) struct _PyInterpreterFrame,該字段底層沒有暴露出來。

PyObject *f_funcobj

指向?qū)?yīng)的函數(shù)對象,該字段解釋器沒有暴露出來。

PyObject *f_globals

指向全局名字空間(一個(gè)字典),它是全局變量的容身之所。是的,Python 的全局變量是通過字典存儲(chǔ)的,調(diào)用函數(shù) globals 即可拿到該字典。

# 等價(jià)于 name = "古明地覺"
globals()["name"] = "古明地覺"

# 等價(jià)于 print(name)
print(globals()["name"])  # 古明地覺

def foo():
    import inspect
    return inspect.currentframe()

frame = foo()
# frame.f_globals 同樣會(huì)返回全局名字空間
print(frame.f_globals is globals())  # True
# 相當(dāng)于創(chuàng)建了一個(gè)全局變量 age
frame.f_globals["age"] = 18
print(age)  # 18

關(guān)于名字空間,我們后面會(huì)用專門的篇幅詳細(xì)說明。

PyObject *f_locals

指向局部名字空間(一個(gè)字典),但和全局變量不同,局部變量不存在局部名字空間中,而是靜態(tài)存儲(chǔ)在數(shù)組中。該字段先有個(gè)印象,后續(xù)再詳細(xì)說。

PyObject *f_builtins

指向內(nèi)建名字空間(一個(gè)字典),顯然一些內(nèi)置的變量都存在里面。

def foo():
    import inspect
    return inspect.currentframe()

frame = foo()
print(frame.f_builtins["list"]("abcd"))
"""
['a', 'b', 'c', 'd']
"""

和我們直接使用 list("abcd") 是等價(jià)的。

PyFrameObject *frame_obj

這個(gè)不用多說,負(fù)責(zé)指向 PyFrameObject 對象。

_Py_CODEUNIT *prev_instr

指向上一條已執(zhí)行完畢的字節(jié)碼指令,比如虛擬機(jī)要執(zhí)行第 n 條指令,那么 prev_instr 便指向第 n - 1 條指令。由于每個(gè)指令都帶有一個(gè)參數(shù),所以 _Py_CODEUNIT 類型的大小是 2 字節(jié)。

int stacktop

表示棧頂相對于 localsplus 數(shù)組的偏移量。

uint16_t return_offset

表示 RETURN 指令相對 prev_instr 的偏移量,這個(gè)值只對被調(diào)用的函數(shù)有意義,它指示了函數(shù)返回后,調(diào)用者應(yīng)該從哪里繼續(xù)執(zhí)行。它會(huì)在 CALL 指令(調(diào)用函數(shù)時(shí))和 SEND 指令(發(fā)送數(shù)據(jù)到協(xié)程或生成器時(shí))中設(shè)置。

這個(gè)設(shè)計(jì)允許更高效的函數(shù)返回處理,因?yàn)樘摂M機(jī)可以直接跳轉(zhuǎn)到正確的位置,而不需要額外的查找或計(jì)算。

def main():
    x = some_func()  # CALL 指令在這里
    y = x + 1     # 函數(shù)返回后應(yīng)該執(zhí)行的下一條指令

def some_func():
    return 42

當(dāng)調(diào)用 some_func 時(shí),虛擬機(jī)會(huì)執(zhí)行 CALL 指令,在 CALL 指令中,會(huì)設(shè)置 return_offset。當(dāng)執(zhí)行完 some_func 的 RETURN 指令時(shí),它會(huì)使用 return_offset 來決定跳轉(zhuǎn)到調(diào)用者(main)中的哪個(gè)位置。

這種機(jī)制的優(yōu)點(diǎn)是不需要在運(yùn)行時(shí)計(jì)算返回位置,因?yàn)樗呀?jīng)在調(diào)用時(shí)預(yù)先計(jì)算好了,特別適用于處理生成器和協(xié)程等復(fù)雜控制流。

char owner

表示幀的所有權(quán)信息,用于區(qū)分幀是在虛擬機(jī)棧上的,還是單獨(dú)分配的。

PyObject *localsplus[1]

一個(gè)柔性數(shù)組,負(fù)責(zé)維護(hù) "局部變量 + cell 變量 + free 變量 + 運(yùn)行時(shí)棧",大小在運(yùn)行時(shí)確定。


以上就是棧幀內(nèi)部的字段,這些字段先有個(gè)印象,后續(xù)在剖析虛擬機(jī)的時(shí)候還會(huì)繼續(xù)細(xì)說。

總之我們看到,PyCodeObject 并不是虛擬機(jī)的最終目標(biāo),虛擬機(jī)最終是在棧幀中執(zhí)行的。每一個(gè)棧幀都會(huì)維護(hù)一個(gè) PyCodeObject 對象,換句話說,每一個(gè) PyCodeObject 對象都會(huì)隸屬于一個(gè)棧幀。并且從 f_back 可以看出,虛擬機(jī)在實(shí)際執(zhí)行時(shí),會(huì)產(chǎn)生很多的棧幀對象,而這些對象會(huì)被鏈接起來,形成一條執(zhí)行環(huán)境鏈表,或者說棧幀鏈表。

而這正是 x64 機(jī)器上棧幀之間關(guān)系的模擬,在 x64 機(jī)器上,棧幀之間通過 RSP 和 RBP 指針建立了聯(lián)系,使得新棧幀在結(jié)束之后能夠順利地返回到舊棧幀中,而 Python 虛擬機(jī)則是利用 f_back 來完成這個(gè)動(dòng)作。

當(dāng)然,獲取棧幀除了通過 inspect 模塊之外,在捕獲異常時(shí),也可以獲取棧幀。

def foo():
    try:
        1 / 0
    except ZeroDivisionError:
        import sys
        # exc_info 返回一個(gè)三元組
        # 分別是異常的類型、值、以及 traceback
        exc_type, exc_value, exc_tb = sys.exc_info()
        print(exc_type)  # <class 'ZeroDivisionError'>
        print(exc_value)  # division by zero
        print(exc_tb)  # <traceback object at 0x00000135CEFDF6C0>

        # 調(diào)用 exc_tb.tb_frame 即可拿到異常對應(yīng)的棧幀
        # 另外這個(gè) exc_tb 也可以通過下面這種方式獲取
        # except ZeroDivisionError as e; e.__traceback__
        print(exc_tb.tb_frame.f_code.co_name)  # foo
        print(exc_tb.tb_frame.f_back.f_code.co_name)  # <module>
        # 顯然 tb_frame 是當(dāng)前函數(shù) foo 的棧幀
        # 那么 tb_frame.f_back 就是整個(gè)模塊對應(yīng)的棧幀
        # 而 tb_frame.f_back.f_back 顯然就是 None 了
        print(exc_tb.tb_frame.f_back.f_back)  # None

foo()

關(guān)于棧幀內(nèi)部的字段的含義,我們就說完了。當(dāng)然如果有些字段現(xiàn)在不是很理解,也沒關(guān)系,隨著不斷地學(xué)習(xí),你會(huì)豁然開朗。


小結(jié)

因?yàn)楹芏鄤?dòng)態(tài)信息無法靜態(tài)地存儲(chǔ)在 PyCodeObject 對象中,所以 PyCodeObject 對象在交給虛擬機(jī)之后,虛擬機(jī)會(huì)在其之上動(dòng)態(tài)地構(gòu)建出 PyFrameObject 對象,也就是棧幀。

因此虛擬機(jī)是在棧幀里面執(zhí)行的字節(jié)碼,它包含了虛擬機(jī)在執(zhí)行字節(jié)碼時(shí)依賴的全部信息。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請遵守用戶 評(píng)論公約

    類似文章 更多