iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
Python

為你自己讀 CPython 原始碼系列 第 18

Day 18 - 虛擬機器大冒險(一)

  • 分享至 

  • xImage
  •  

本文同步刊載於 「為你自己學 Python - 虛擬機器大冒險(一)

虛擬機器大冒險(一)

為你自己學 Python

Python 虛擬機器(Python Virtual Machine, PVM)是 Python 程式碼背後運行的核心,負責解讀並執行我們所寫的每一行 Python 程式碼。從 Bytecode 轉換成具體操作的指令、物件的建立與銷毀、記憶體管理等等都算是它的守備範圍。因此,接下來的幾個章節我會試著用幾行簡單的程式,理解 Python 虛擬機器的運作原理。

我們在前面已經介紹過直譯器啟動的過程,從把檔案讀進來、轉換成 AST 再轉換成 Bytecode,最後再交給虛擬機器執行,所以接下來就讓我們從函數開始吧!

在 Python 裡,函數是使用 def 關鍵字定義的程式碼區塊,至於為什麼要寫函數或是寫函數有什麼好處,可參閱「為你自己學 Python」的函數 - 基礎篇章節介紹。這個章節主要要來看看在 CPython 是怎麼定義一個函數,函數物件裡又藏了哪些好玩的東西,以及函數執行的時候發生了什麼事。

函數也是物件

在 Python 裡函數也是物件,既然是物件,那應該就能找到對應的型別結構:

PyTypeObject PyFunction_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "function",
    // ...  略 ...
    (reprfunc)func_repr,                        /* tp_repr */
    0,                                          /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    0,                                          /* tp_hash */
    PyVectorcall_Call,                          /* tp_call */
    0,                                          /* tp_str */
    // ... 略 ...
    0,                                          /* tp_dict */
    func_descr_get,                             /* tp_descr_get */
    0,                                          /* tp_descr_set */
    offsetof(PyFunctionObject, func_dict),      /* tp_dictoffset */
    0,                                          /* tp_init */
    0,                                          /* tp_alloc */
    func_new,                                   /* tp_new */
};

看的出來這個型別實作的功能不多,像是 tp_as_ 這三個成員變數都是空的,表示它不會被當數字、序列或是對映類型的資料來操作。這也合理的確函數沒有也不需要像字串、串列、字典或 Tuple 一樣的行為,它只要做好它的本分工作,就是接收參數、執行函數並且回傳應該回傳的值就好。

雖然是這樣,這個 PyFunctionObject 型別結構裡的東西倒是不少:

// 檔案:Include/cpython/funcobject.h

typedef struct {
    PyObject_HEAD
    _Py_COMMON_FIELDS(func_)
    PyObject *func_doc;         /* The __doc__ attribute, can be anything */
    PyObject *func_dict;        /* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;      /* The __module__ attribute, can be anything */
    PyObject *func_annotations; /* Annotations, a dict or NULL */
    PyObject *func_typeparams;  /* Tuple of active type variables or NULL */
    vectorcallfunc vectorcall;
    uint32_t func_version;
} PyFunctionObject;

如果把 _Py_COMMON_FIELDS(func_) 展開,整個 PyFunctionObject 看起來像這樣:

// 檔案:Include/cpython/funcobject.h

typedef struct {
    PyObject_HEAD

    // Py_COMMON_FIELDS
    PyObject *func_globals;
    PyObject *func_builtins;
    PyObject *func_name;
    PyObject *func_qualname;
    PyObject *func_code;        /* A code object, the __code__ attribute */
    PyObject *func_defaults;    /* NULL or a tuple */
    PyObject *func_kwdefaults;  /* NULL or a dict */
    PyObject *func_closure;     /* NULL or a tuple of cell objects */

    PyObject *func_doc;         /* The __doc__ attribute, can be anything */
    PyObject *func_dict;        /* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;      /* The __module__ attribute, can be anything */
    PyObject *func_annotations; /* Annotations, a dict or NULL */
    PyObject *func_typeparams;  /* Tuple of active type variables or NULL */
    vectorcallfunc vectorcall;
    uint32_t func_version;
} PyFunctionObject;

有些成員光看名字就很好猜,不過這個 func_code 成員變數,後面的註解寫著它是一個 Code Object,這傢伙已經看它出現好多次了,它應該就是函數的本體。在 Python 裡你可以把函數想像成它是一個帶有名字的盒子,當我們連名帶姓的呼喊這個函數的名字的時候,本質上就是把這顆函數物件裡的 Code Object 交給虛擬機器執行而已。問題是,這個 Code Object 是怎麼建立的?或是,它是什麼時候建立的?我們來寫個簡單的函數試試看..

準備建立函數

先來個打招呼的函數:

def greeting(name):
    print(f"Hello, {name}")

它的 Bytecode 看起來會是這樣:

  1           2 LOAD_CONST               0 (<code object>)
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (greeting)
              8 RETURN_CONST             1 (None)

看起來應該是由 MAKE_FUNCTION 指令負責建立函數,指令的名字也滿直白的,但是在 MAKE_FUNCTION 指令之前有個 LOAD_CONST 指令載入了一顆 Code Object。我們之前也看過類似的指令,表示這顆 Code Object 是在編譯階段,也就是在 AST 轉換成 Bytecode 的過程中先就建立好了,這裡才能被「載入」,並且透過 MAKE_FUNCTION 指令把它包在函數裡。我們在「從準備到起飛!」章節講到 AST 到 Bytecode 的過程中,曾經追過 _PyAST_Compile() 函數,它的回傳值剛好就是一個 Code Object。

看起來想知道函數是怎麼回事,得先花點時間研究這個 Code Object,所以我們就晚點再來看函數,先看看 Code Object 長什麼樣子。

程式碼物件

光看 Code Object 名字就能知道它是一顆物件,照 CPython 的命名慣例,應該不難猜到這個物件叫做 PyCodeObject,翻一下原始碼會發現它被定義成巨集:

// 檔案:Include/cpython/code.h

#define _PyCode_DEF(SIZE) {                                                    \
    PyObject_VAR_HEAD                                                          \
                                                                               \
    // ... 略 ...
    /* The hottest fields (in the eval loop) are grouped here at the top. */   \
    PyObject *co_consts;           /* list (constants used) */                 \
    PyObject *co_names;            /* list of strings (names used) */          \
    PyObject *co_exceptiontable;   /* Byte string encoding exception handling  \
                                      table */                                 \
    // ... 略 ...
}

是一般的結構或是巨集都無所謂,但這個結構還不小,裡面有些成員在「為你自己學 Python」的函數 - 進階篇章節裡出現過,像是 co_constsco_names。接著來看看這個物件怎麼建立的。在 Python/compile.c 檔案開頭的有一段解註解是這樣寫的:

The primary entry point is _PyAST_Compile(), which returns a
PyCodeObject. The compiler makes several passes to build the code
object:
  1. Checks for future statements.  See future.c
  2. Builds a symbol table.  See symtable.c.
  3. Generate an instruction sequence. See compiler_mod() in this file.
  4. Generate a control flow graph and run optimizations on it.  See flowgraph.c.
  5. Assemble the basic blocks into final code.  See optimize_and_assemble() in
     this file, and assembler.c.

一開始會先檢查有沒有「未來」陳述句,就是 from __future__ import ... 的寫法,所謂的「未來模組」主要是讓 Python 2 的程式碼能夠在 Python 3 上運行,這個步驟主要是為了確保程式碼的相容性。接著會建立符號表(Symbol Table),符號表裡會記錄程式裡會用到的變數、函數或是類別等資訊。

再來是把 AST 轉成中間碼(Intermediate Code),再進行一些流程的分析跟最佳化,這感覺有點複雜,不過最後組裝的過程是在 Python/compile.coptimize_and_assemble() 函數中開始,Code Object 應該也是在這時候組裝起來,我們就從這裡開始追看看:

檔案:Python/compile.c

static PyCodeObject *
optimize_and_assemble(struct compiler *c, int addNone)
{
    struct compiler_unit *u = c->u;
    PyObject *const_cache = c->c_const_cache;
    PyObject *filename = c->c_filename;

    int code_flags = compute_code_flags(c);

    // ... 略 ...

    return optimize_and_assemble_code_unit(u, const_cache, code_flags, filename);
}

果然這個函數就是建立並回傳 PyCodeObject 的地方。順著 optimize_and_assemble_code_unit() 往下追:

// 檔案:Python/compile.c

static PyCodeObject *
optimize_and_assemble_code_unit(struct compiler_unit *u, PyObject *const_cache,
                   int code_flags, PyObject *filename)
{
    // ... 略 ...
    co = _PyAssemble_MakeCodeObject(&u->u_metadata, const_cache, consts,
                                    maxdepth, &optimized_instrs, nlocalsplus,
                                    code_flags, filename);

    // ... 略 ...
}

這個函數的行數稍微有點多,前半段大多都是在做一些準備工作,最後的 _PyAssemble_MakeCodeObject() 函數就是把前面準備好的資料進行組裝,並產生 Code Object:

// 檔案:Python/assemble.c

PyCodeObject *
_PyAssemble_MakeCodeObject(_PyCompile_CodeUnitMetadata *umd, PyObject *const_cache,
                           PyObject *consts, int maxdepth, instr_sequence *instrs,
                           int nlocalsplus, int code_flags, PyObject *filename)
{
    PyCodeObject *co = NULL;

    struct assembler a;
    int res = assemble_emit(&a, instrs, umd->u_firstlineno, const_cache);
    if (res == SUCCESS) {
        co = makecode(umd, &a, const_cache, consts, maxdepth, nlocalsplus,
                      code_flags, filename);
    }
    assemble_free(&a);
    return co;
}

就是它了,makecode() 函數就是建立 Code Object 的地方:

// 檔案:Python/assemble.c

static PyCodeObject *
makecode(_PyCompile_CodeUnitMetadata *umd, struct assembler *a, PyObject *const_cache,
         PyObject *constslist, int maxdepth, int nlocalsplus, int code_flags,
         PyObject *filename)
{
    PyCodeObject *co = NULL;
    PyObject *names = NULL;
    PyObject *consts = NULL;
    PyObject *localsplusnames = NULL;

    // ... 略 ...
    consts = PyList_AsTuple(constslist); /* PyCode_New requires a tuple */

    // ... 略 ...
    localsplusnames = PyTuple_New(nlocalsplus);

    struct _PyCodeConstructor con = {
        // ... 略 ...
        .consts = consts,
        .names = names,
        .localsplusnames = localsplusnames,
    };

    // ... 略 ...
    co = _PyCode_New(&con);

    // ... 略 ...
    return co;
}

這個函數裡有幾個重點,首先,會把在這裡用到的常數跟區域變數用 Tuple 型態來存放,最後再呼叫 _PyCode_New() 函數來建立 Code Object:

// 檔案:Objects/codeobject.c

PyCodeObject *
_PyCode_New(struct _PyCodeConstructor *con)
{
    // ... 略 ...
    Py_ssize_t size = PyBytes_GET_SIZE(con->code) / sizeof(_Py_CODEUNIT);
    PyCodeObject *co = PyObject_NewVar(PyCodeObject, &PyCode_Type, size);
    if (co == NULL) {
        Py_XDECREF(replacement_locations);
        PyErr_NoMemory();
        return NULL;
    }
    init_code(co, con);
    Py_XDECREF(replacement_locations);
    return co;
}

可以看到這個函數建立了一顆 PyCodeObject 物件,最後再把前面收集到的資訊透過 init_code() 函數進行初始化:

// 檔案:Objects/codeobject.c

static void
init_code(PyCodeObject *co, struct _PyCodeConstructor *con)
{
    // ... 略 ...
    co->co_filename = Py_NewRef(con->filename);
    co->co_name = Py_NewRef(con->name);
    co->co_qualname = Py_NewRef(con->qualname);
    co->co_flags = con->flags;

    // ... 略 ...
    co->co_consts = Py_NewRef(con->consts);
    co->co_names = Py_NewRef(con->names);

    // ... 略 ...
}

這樣,就完成了 Code Object 的建立。我們可以進到 REPL 裡試玩看看:

$ python -i hi.py
>>> greeting.__code__
<code object greeting at 0x104f97130>
>>> greeting.__code__.co_name
'greeting'
>>> greeting.__code__.co_consts
(None, 'Hello, ')

在 Python 裡可以透過 __code__ 取得這個函數的 Code Object,剛剛最後進行初始化的那些值,像是 co_nameco_consts 都可以透過這個 Code Object 取得。這樣我們就可以知道這個函數的名字是 greeting,而且在這個函數裡使用到的常數是 None'Hello, '

常數 'Hello, ' 還可以理解,各位可以猜猜看在這個函數裡面什麼時候用到 None 了呢?

本文同步刊載於 「為你自己學 Python - 虛擬機器大冒險(一)


上一篇
Day 17 - 不動如山的 Tuple
下一篇
Day 19 - 虛擬機器大冒險(二)
系列文
為你自己讀 CPython 原始碼21
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言