本文同步刊載於 「為你自己學 Python - 虛擬機器五部曲(三)」
在前面的章節簡單的看了一下 Code Object 和函數物件的結構,大概可以知道每個函數裡面會包一個 Code Object,而這個 Code Object 就是實際被執行的最小單位。我們從最底下的 Code Object 往上層追到函數,接下來我們要繼續再往上層追,看看函數執行的過程發生什麼事。
一般程式語言在執行函數的時候,會建立一個新的執行環境,這個執行環境包含了當前函數的所有必要資訊,如本地變數、全域變數等。這個執行環境有些程式語言會稱它叫「呼叫堆疊(Call Stack)」,在 Python 裡被稱為 Frame Object。每當呼叫一個函數,就會建立一個新的 Frame。Frame 會被放在一個堆疊裡,當函數執行完畢後,這顆 Frame 會被移除。
我們就來看看在 CPython 裡 Frame 長什麼樣子,以及它的出生與銷毀的過程是怎麼回事。
// 檔案: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;
PyObject *_f_frame_data[1];
};
除了大家都有的 PyObject_HEAD
之外還有幾個比較值得介紹看的,其中 f_back
會指向堆疊中的前一個 Frame Object,一個接一個,可以形成一個鏈表結構。f_frame
是一個指向 _PyInterpreterFrame
型別的指標,嗯...這個待會再來追。中間還有一些看起來像是程式碼行號的東西,最後面的 _f_frame_data[1]
成員,我們在前面章節有看過類似的設計,這是一個彈性陣列成員(Flexible Array Member),用於存儲額外的 frame 相關資料。
來看看 _PyInterpreterFrame
的定義:
// 檔案:Include/internal/pycore_frame.h
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;
喔喔喔,在這個結構裡有看到我們前面看過的 Code Object f_code
,然後它也有個 previous
指向前一個結構,形成執行堆疊。f_funcobj
指向與此這個 Frame 關聯的函數物件。
這裡還能看到 f_globals
、f_builtins
以及 f_locals
這三個物件,從名字大概就能看出來分別是用來儲存全域變數、內建變數以及區域變數。
最後還有一個 localsplus
,這也是一個彈性陣列成員,事實上它才是用來存放局部變數的地方。f_locals
通常是一個字典結構,大部份時候它都是 NULL
,只有當真正需要以字典形式訪問局部變數時(例如呼叫 locals() 函數),才會建立 f_locals
字典,而當需要建立 f_locals
字典時,localsplus
裡的值就會被複製或填充到 f_locals
裡。
當呼叫一個 Python 函數的時候,會產生一個新的 PyFrameObject
和 _PyInterpreterFrame
。PyFrameObject
的 f_frame
成員指向 _PyInterpreterFrame
,而且 _PyInterpreterFrame
的 frame_obj
成員反過來指向 PyFrameObject
。
要追蹤 Frame Object 的建立,大概是從 _PyEval_EvalFrameDefault
這個函數開始,但還沒看始追,一開頭寫著有點嚇人的註解:
_PyEval_EvalFrameDefault() is a *big* function
看了一下是真的不小,大概有 350 行左右:
// 檔案:Python/ceval.c
PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag)
{
// ... 略 ...
}
我試著從這裡拆解一些重點出來:
// 檔案:Python/ceval.c
PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag)
{
_PyInterpreterFrame entry_frame;
_PyCFrame *prev_cframe = tstate->cframe;
// ... 略 ...
entry_frame.f_code = tstate->interp->interpreter_trampoline;
entry_frame.prev_instr =
_PyCode_CODE(tstate->interp->interpreter_trampoline);
entry_frame.stacktop = 0;
entry_frame.owner = FRAME_OWNED_BY_CSTACK;
entry_frame.return_offset = 0;
entry_frame.previous = prev_cframe->current_frame;
frame->previous = &entry_frame;
}
一進來先建立一個新的 entry_frame
,同時也透過 PyThreadState
取得當前的堆疊。在過程中把 entry_frame
的一些成員設定好,透過把 entry_frame
的 previous
指向前一個剛才取得的堆疊裡的 Frame 做到把 entry_frame
推到堆疊裡的效果,最後再把 frame
的 previous
指向剛剛建立的這個 entry_frame
。有點暈,但這樣就建立了一個新的 Frame Object。
接下來,在中間有一段格式看起來有點難懂的:
// 檔案:Python/ceval.c
PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag)
{
/* Start instructions */
#if !USE_COMPUTED_GOTOS
dispatch_opcode:
switch (opcode)
#endif
{
#include "generated_cases.c.h"
// ... 略 ...
#if USE_COMPUTED_GOTOS
TARGET_INSTRUMENTED_LINE:
#else
case INSTRUMENTED_LINE:
#endif
// ... 略 ...
} /* End instructions */
}
從 /* Start instructions */
到 /* End instructions */
這整段,是一個大型的 switch
語法,裡面有很多不同的 case
,每個 case
代表一個指令,特別注意 #include "generated_cases.c.h"
這一行,這個引入了大量生成的操作碼。這個生成的檔案追進去看大概有快 4,800 多行,一開頭的註解也寫到這個檔案是怎麼生成的:
// This file is generated by Tools/cases_generator/generate_cases.py
// from:
// Python/bytecodes.c
// Do not edit!
還叫我們不要自己手動編輯,有興趣可以再去追一下生成這個檔案的 generate_cases.py
。
最後來看看 Frame Object 的銷毀:
// 檔案:Python/ceval.c
PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag)
{
// ... 略 ...
exit_unwind:
assert(_PyErr_Occurred(tstate));
_Py_LeaveRecursiveCallPy(tstate);
assert(frame != &entry_frame);
// GH-99729: We need to unlink the frame *before* clearing it:
_PyInterpreterFrame *dying = frame;
frame = cframe.current_frame = dying->previous;
_PyEvalFrameClearAndPop(tstate, dying);
frame->return_offset = 0;
if (frame == &entry_frame) {
/* Restore previous cframe and exit */
tstate->cframe = cframe.previous;
assert(tstate->cframe->current_frame == frame->previous);
tstate->c_recursion_remaining += PY_EVAL_C_STACK_UNITS;
return NULL;
}
// ... 略 ...
}
exit_unwind
這個標籤大概就是做準備撤退的收尾工作,這裡有段註解這樣寫著:
// GH-99729: We need to unlink the frame *before* clearing it:
這表明在斷開 Frame Object 之前,要先把這傢伙從整串的 Frame Object 鏈拆下來。所以這兩行:
_PyInterpreterFrame *dying = frame;
在做的事情就是建立一個新的 _PyInterpreterFrame
物件,會叫 dying
是因為它就是快要 GG 的。而接下來這行:
frame = cframe.current_frame = dying->previous;
原本的 Frame 是串串相連到天邊,這行程式的意思就是把後面的 Frame Object 指向 dying
的前一個 Frame Object,這樣就把 dying
從整串 Frame Object 鏈拆下來了。
最後,就是呼叫 _PyEvalFrameClearAndPop()
函數來做清理善後的事了。
Frame Object 從它誕生的那一刻,就註定要過著忙碌又短暫的生活。我們每次呼叫函數的背後都有一個 Frame Object 幫我們打理搬運變數、執行指令,還要隨時準備處理意外狀況這些雜事,它們的辛苦換來了我們程式的正常運行。下次當你呼叫一個函數時,別忘了向這些幕後英雄們說聲謝謝!
本文同步刊載於 「為你自己學 Python - 虛擬機器五部曲(三)」