本文同步刊載於 「為你自己學 Python - 物件生成全紀錄」
class Cat:
pass
kitty = Cat()
這段 Python 程式碼看起來很簡單,像這樣在 Python 中,定義一個類別並建立實體(instance)是很常見的操作。不過,你知道透過 Cat
類別產生 kitty
實體的過程中,背後發生了什麼事嗎?在「物件導向程式設計 - 進階篇」裡曾經跟大家介紹過會經歷 __new__
及 __init__
這兩個階段,以及這兩個方法的差別,接下來我們就從 CPython 原始碼的角度來看看發生什麼事。
Python 在執行我們的程式碼之前雖然不需要像 C 語言一樣進行編譯,但 Python 直譯器總得先讀懂我們寫的程式碼才行。首先第一步是進行 Tokenization。Tokenization 的過程我找不到比較合適的中文翻譯,簡單來說就是把我們寫的程式碼轉拆成一個個的 Token,我用個最簡單的例子:
a = 1 + 2
這段程式碼會被拆解成如下的 Token:
這個過程的目的是將文字形式的原始程式碼,轉換成語法意圖清晰且可理解的片段。在 CPython 裡負責做這些事的原始碼在 Parser/tokenizer.c
,在這裡主要負責做這件事的是 _PyTokenizer_Get()
函數:
// 檔案:Parser/tokenizer.c
int _PyTokenizer_Get(struct tok_state *tok, struct token *token) {
int result = tok_get(tok, token);
if (tok->decoding_erred) {
result = ERRORTOKEN;
tok->done = E_DECODE;
}
return result;
}
裡面的 tok_get()
函數就是負責把原始碼轉換成 Token 的函數,這個函數裡面有很多細節,包括處理一般的文字跟「F 字串(F-String)」。而 Token 的列表是在 Grammar/Tokens
裡,所以如果想要幫 Python 加入新的關鍵字或是語法的話,可以從這兩個地方下手。
原本的純文字被轉換成一個個的 Token 之後,接著會把 Token 再轉換為「抽象語法樹(Abstract Syntax Tree, AST)」。轉換成 AST 的目的,是為了讓直譯器能夠理解我們的程式碼。
在前兩章曾經介紹過整個 CPython 的專案結構中,有個叫做 Grammar
以及 Parser
這兩個目錄,裡面的程式就是負責這個工作的。不過把語法轉換成 AST 的細節有點複雜,超過我目前的能力範圍,這裡我們只要先知道會發生這件事就好。
接下來,剛才解析好的 AST 會被編譯為 Bytecode。咦?編譯?Python 不是直譯語言嗎?沒錯,Python 是直譯語言,但是 Python 直譯器會把我們寫的程式碼編譯成一種叫做 Bytecode 的中間碼,然後再執行這些 Bytecode。
單一個 .py
檔案可能沒感覺,舉個例子,我有 a.py
跟 b.py
兩個檔案,如果 a.py
裡有 import b
的話,b.py
裡的程式碼會被編譯成 Bytecode,然後被存儲在 __pycache__
目錄裡的 b.cpython-312.pyc
檔案裡,這個 .pyc
檔案的命名規則還滿好猜的,cpython-312
這個部分就是 Python 直譯器的版本號,312
就是 Python 3.12 的意思。
所以如果你曾經在專案裡看到像是 __pycache__
這樣的目錄的話,這些就是編譯過的 Bytecode 啦。當再次執行同樣的 Python 程式時,Python 直譯器會先檢查是否有對應的 .pyc
檔案以及是否需要重新再編譯一次,如果有的話而且不用重新編譯就會直接載入這些 Bytecode,由 Python 虛擬機器(Virtual Machine)解譯、執行。
把 AST 編譯成 Bytecode 的原始碼在 CPython 專案裡的位置是 Python/compile.c
,有興趣的話可以追一下這個檔案裡的 _PyAST_Compile()
函數,這個函數會把 AST 轉換成可以執行的 Code Object。關於 Code Object,在「為你自己學 Python」 的函數 - 進階篇章節有稍微介紹過,這裡就不再贅述。
這裡我們可以試著用 Python 內建的 dis
模組來看看我們寫的程式碼被編譯成什麼樣的 Bytecode,可以執行以下指令:
$ python -m dis demo.py
印出來的結果如下:
0 0 RESUME 0
1 2 PUSH_NULL
4 LOAD_BUILD_CLASS
6 LOAD_CONST 0 (<code object Cat at 0x1045bb5d0, file "demo.py", line 1>)
8 MAKE_FUNCTION 0
10 LOAD_CONST 1 ('Cat')
12 CALL 2
20 STORE_NAME 0 (Cat)
4 22 PUSH_NULL
24 LOAD_NAME 0 (Cat)
26 CALL 0
34 STORE_NAME 1 (kitty)
36 RETURN_CONST 2 (None)
Disassembly of <code object Cat at 0x1045bb5d0, file "demo.py", line 1>:
1 0 RESUME 0
2 LOAD_NAME 0 (__name__)
4 STORE_NAME 1 (__module__)
6 LOAD_CONST 0 ('Cat')
8 STORE_NAME 2 (__qualname__)
2 10 RETURN_CONST 1 (None)
最前面的 0
、1
以及 4
,表示這是在原始碼裡的行號。這裡可先看兩個重點,首先 LOAD_BUILD_CLASS
是用來建立類別,再透過 STORE_NAME
把類別存到變數 Cat
裡。而 LOAD_BUILD_CLASS
實際上對應到的 CPython 原始碼是這段:
// 檔案:Python/bltinmodule.c
static PyObject *
builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
PyObject *kwnames)
{
PyObject *func, *name, *winner, *prep;
// ... 略 ...
return cls;
}
這個函數比較長一點,我先挑一小段出來:
// 檔案:Python/bltinmodule.c
if (meta == NULL) {
/* if there are no bases, use type: */
if (PyTuple_GET_SIZE(bases) == 0) {
meta = (PyObject *) (&PyType_Type);
}
/* else get the type of the first base */
else {
PyObject *base0 = PyTuple_GET_ITEM(bases, 0);
meta = (PyObject *)Py_TYPE(base0);
}
Py_INCREF(meta);
isclass = 1; /* meta is really a class */
}
從這段可以看出來如果沒有指定 meta class,而且沒有指定上層類別的話(例如範例裡的 Cat
類別),那麼它的 Metaclass 就會是 type
,也就是 PyType_Type
。關於 Metaclass 可參閱「為你自己學 Python」的物件導向程式設計 - 進階篇章節。
這個 builtin___build_class__()
函數裡還有很多東西值得介紹的,不過就等後續細講到物件導向的時候再來說明。簡單的說,這個函數會回傳一個類別,但仔細看就會發現,所謂的類別也就是一個 PyObject
,也就是說,在 Python 裡類別也是一種物件。
類別建立好了,再來就是準備建立實體了。
在剛才的 Byteocde 的後半段可以看到這個:
// ... 略 ...
10 LOAD_CONST 1 ('Cat')
12 CALL 2
20 STORE_NAME 0 (Cat)
4 22 PUSH_NULL
24 LOAD_NAME 0 (Cat)
26 CALL 0
34 STORE_NAME 1 (kitty)
36 RETURN_CONST 2 (None)
// ... 略 ...
當執行 kitty = Cat()
的時候,實際上就是「呼叫(Call)」這個類別。同樣在物件導向程式設計 - 進階篇有介紹過幾個魔術方法,只要這個物件身上有 __call__
這個魔術方法,這顆物件就能被「呼叫」。
雖然我自己寫的 Cat
類別並沒有實作 __call__
這個魔術方法,但是建立 Cat
類別的 Metaclass 就是 type
類別,所以「呼叫」或「執行」Cat
類別的時候,看的就是 type
類別的 tp_call
成員變數:
// 檔案:Objects/typeobject.c
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
sizeof(PyHeapTypeObject), /* tp_basicsize */
sizeof(PyMemberDef), /* tp_itemsize */
(destructor)type_dealloc, /* tp_dealloc */
// ... 略 ...
0, /* tp_hash */
(ternaryfunc)type_call, /* tp_call */
0, /* tp_str */
(getattrofunc)_Py_type_getattro, /* tp_getattro */
.tp_vectorcall = type_vectorcall,
};
可以看的出來這個結構的成員變數 tp_call
指向一個名為 type_call
的函數,再繼續往下追:
// 檔案:Objects/typeobject.c
static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyObject *obj;
// ... 略 ...
obj = type->tp_new(type, args, kwds);
obj = _Py_CheckFunctionResult(tstate, (PyObject*)type, obj, NULL);
if (obj == NULL)
return NULL;
// ... 略 ...
type = Py_TYPE(obj);
if (type->tp_init != NULL) {
int res = type->tp_init(obj, args, kwds);
if (res < 0) {
assert(_PyErr_Occurred(tstate));
Py_SETREF(obj, NULL);
}
else {
assert(!_PyErr_Occurred(tstate));
}
}
return obj;
}
在這個 type_call()
函數裡,會呼叫 type->tp_new()
函數來產生物件,如果沒出錯,待會就會呼叫 type->tp_init()
函數來初始化這個物件。這裡的 tp_new()
跟 tp_init()
,跟我們在 Python 裡學過的 __new__
以及 __init__
是對的起來的。
所以,如果是使用我們自己寫的類別來建立實體,背後的流程會經過:
編譯階段:
執行階段:
tp_call()
type
類別預設的 tp_call()
會呼叫 tp_new()
函數來建立物件。tp_init()
函數來初始化物件。順帶一提,type
這傢伙很有趣,它本身是個物件也是個類別,但我們之前常會用 type(123)
這樣的寫法來取得類別名稱,這正是因為在剛才介紹到的 type_call()
函數裡有一段特別的程式碼:
// 檔案:Objects/typeobject.c
/* Special case: type(x) should return Py_TYPE(x) */
/* We only want type itself to accept the one-argument form (#27157) */
if (type == &PyType_Type) {
assert(args != NULL && PyTuple_Check(args));
assert(kwds == NULL || PyDict_Check(kwds));
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
if (nargs == 1 && (kwds == NULL || !PyDict_GET_SIZE(kwds))) {
obj = (PyObject *) Py_TYPE(PyTuple_GET_ITEM(args, 0));
return Py_NewRef(obj);
}
/* SF bug 475327 -- if that didn't trigger, we need 3
arguments. But PyArg_ParseTuple in type_new may give
a msg saying type() needs exactly 3. */
if (nargs != 3) {
PyErr_SetString(PyExc_TypeError,
"type() takes 1 or 3 arguments");
return NULL;
}
}
如果是帶 3 個參數給它的話,可以透過 type()
來建立一個新的類別,但是如果只帶 1 個參數的話,type()
就會回傳這個參數的類別,這也就是為什麼 type(123)
會回傳 <class 'int'>
而 type('hello kitty')
會得到 <class 'str'>
的原因。
本文同步刊載於 「為你自己學 Python - 物件生成全紀錄」