虛擬機器五部曲(二)
上個章節大概看過包在函數裡的 Code Object,Code Object 是在編譯過程就先建立好,在程式執行的時候透過 LOAD_CONST
指令載入並被包進函數裡,而函數應該就不是了,它是在執行過程中透過 MAKE_FUNCTION
指令建立的,接下來就再來看看函數物件本身是怎麼回事。
建立函數物件
在 Python 裡的函數物件長這個樣子:
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__
屬性。但是要怎麼建立函數物件呢?在上個章節我們也有看到函數物件是透過 MAKE_FUNCTION
指令產生的,函數物件裡包含了函數的名稱、參數、預設值、程式碼。來看看這個指令做了什麼事:
inst(MAKE_FUNCTION, (defaults if (oparg & 0x01),
kwdefaults if (oparg & 0x02),
annotations if (oparg & 0x04),
closure if (oparg & 0x08),
codeobj -- func)) {
PyFunctionObject *func_obj = (PyFunctionObject *)
PyFunction_New(codeobj, GLOBALS());
// ... 略 ...
if (oparg & 0x08) {
assert(PyTuple_CheckExact(closure));
func_obj->func_closure = closure;
}
// ... 略 ...
}
嗯...看的出來這個指令就是建立一顆 PyFunctionObject
物件,但後面有一連串的 oparg
的位元運算,那是在幹嘛?這個 oparg
又是什麼?
參數長什麼樣子?
其實,這個 oparg
是在編譯期間就已經決定好的,大家回頭來看看編譯出好的 Bytecode 指令:
1 2 LOAD_CONST 0 (<code object>)
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (greeting)
8 RETURN_CONST 1 (None)
在 MAKE_FUNCTION
指令後面的那個 0
就是 oparg
,主要的用途是告訴虛擬機器這個函數物件的屬性。oparg
是一個 8 位元的整數,不過目前只使用了最低的 4 位。不同的函數可能會有不同的屬性,例如原本的範例只帶了個簡單的參數,它的 oparg
是 0
,如果改成這樣:
def greeting(name="Kitty"):
print(f"Hello, {name}")
這是個帶有預設參數的函數,這時候 oparg
就是 1
,也就是 0001
,如果再改成這樣:
def greeting(name: str):
print(f"Hello, {name}")
加上了型別註記(Type Annotation),這時候 oparg
就是 4
,也就是 0100
,如果再改成這樣:
def greeting(name: str = "Kitty"):
print(f"Hello, {name}")
這樣 oparg
就是 5
,也就是 0101
,這是 0001
和 0100
的位元運算結果。透過 oparg
可以告訴虛擬機器這個函數物件大概長什麼樣子。
所以在 MAKE_FUNCTION
指令裡面後半段一連串的位元運算就是在做這件事。
存取函數屬性
是說,為什麼透過函數的 __name__
可以取得函數的名字,__code__
可以取得函數裡面包的那顆 Code Object,__annotations__
可以取得型別註記,這些屬性是怎麼對應到函數物件的?
還記得在上個章節有看過 PyFunction_Type
這個型別嗎?雖然它沒有 tp_as_
之類的成員,但如果要透過屬性存取的時候,要看的成員是 tp_getset
,它對應到 func_getsetlist
:
static PyGetSetDef func_getsetlist[] = {
{"__code__", (getter)func_get_code, (setter)func_set_code},
{"__defaults__", (getter)func_get_defaults,
(setter)func_set_defaults},
{"__kwdefaults__", (getter)func_get_kwdefaults,
(setter)func_set_kwdefaults},
{"__annotations__", (getter)func_get_annotations,
(setter)func_set_annotations},
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict},
{"__name__", (getter)func_get_name, (setter)func_set_name},
{"__qualname__", (getter)func_get_qualname, (setter)func_set_qualname},
{"__type_params__", (getter)func_get_type_params,
(setter)func_set_type_params},
{NULL} /* Sentinel */
};
這裡看到的 getter
跟 setter
,就是用來取得跟設定屬性的函數,翻一下 __code__
對應到的 func_get_code
函數:
static PyObject *
func_get_code(PyFunctionObject *op, void *Py_UNUSED(ignored))
{
if (PySys_Audit("object.__getattr__", "Os", op, "__code__") < 0) {
return NULL;
}
return Py_NewRef(op->func_code);
}
滿簡單的,就是回傳 PyFunctionObject
結構裡的 func_code
屬性。其他的屬性也是類似的,例如 __name__
屬性:
static PyObject *
func_get_name(PyFunctionObject *op, void *Py_UNUSED(ignored))
{
return Py_NewRef(op->func_name);
}
是說,既然這裡有 setter 可以用,我是不是可以做到在執行階段動態的改變函數裡的 Code Object,讓函數的行為改變呢?來試試看:
def greeting(name):
print(f"Hello, {name}")
greeting("Kitty") # Hello, Kitty
secret_object = compile('print("Hey Hey!")', __name__, "exec")
greeting.__code__ = secret_object
greeting()
其實並不是隨便什麼東西都能往 __code__
裡面放的,有興趣可追一下 func_set_code
函數,裡面有一些檢查機制。不管如何,上面這段範例執行之後的確會印出 Hey Hey!
,所以的確是做的到的,只是我目前想不太到什麼情境會需要這樣做。
呼叫函數
我們之前在看 PyType_Type
結構的時候有看過一個 tp_call
成員,當試著要對某個物件施展小括號 ()
這個動作的時候,會觸發這個成員。函數物件也有這個成員,來看看 PyFunction_Type
的 tp_call
成員,它指向 PyVectorcall_Call()
函數。先不看原始碼,先看看這名字,Vector
通常我們會翻譯成「向量」,但呼叫函數跟向量有什麼關係?