物件生成全紀錄
首先,先來看一段簡單到不行但沒什麼用的 Python 程式碼:
class Cat:
pass
kitty = Cat()
這段 Python 程式碼看起來很簡單,像這樣在 Python 中定義一個類別並建立實體(instance)是很常見的操作。不過,你知道透過 Cat
類別建立 kitty
實體的過程中,背後發生了什麼事嗎?如果各位曾經寫過 Python 的類別,應該都寫過 __init__()
這個函數,大概也知道它是什麼用途,但就不一定知道除了 __init__()
之外,還有個跟它有點像但用途不同的 __new__()
函數,接下來我們就從 CPython 原始碼的角度來看看物件被建立的時候發生了什麼事,以及這兩個函數有什麼差別。
程式跑起來!
Step 0 程式碼解析
Python 程式雖然不需要像 C 語言一樣進行編譯,但 Python 直譯器同樣得要先讀懂我們寫的程式碼。首先第一步是進行 Tokenization。Tokenization 的過程我找不到比較合適的白話文翻譯,簡單來說就是把我們寫的程式碼轉拆成一個一個的 Token,我用個最簡單的例子:
a = 1 + 2
這段程式碼會被拆解成如下的 Token:
- NAME (a)
- EQUAL (=)
- NUMBER (1)
- PLUS (+)
- NUMBER (2)
這個過程的目的是將文字形式的原始程式碼,轉換成語法意圖清晰且可理解的片段。在 CPython 裡負責做這些事的原始碼在 Parser/tokenizer.c
,重點在 _PyTokenizer_Get()
這個函數裡:
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 加入新的關鍵字或是語法的話,可以從這兩個地方下手。
Step 1 轉換成 AST
我們寫的程式碼被轉換成一個一個的 Token 之後,接著會把 Token 轉換為「抽象語法樹(Abstract Syntax Tree, AST)」。轉換成 AST 的目的,是為了讓直譯器能夠理解我們的程式碼。
在前面章節曾經介紹過在 CPython 的專案結構中,分別有叫做 Grammar
以及 Parser
這兩個目錄,在這兩個目錄裡的程式就是負責做這些工作的。不過把語法轉換成 AST 的細節有點複雜,超過我們這本書要介紹的範圍,這裡我們只要先知道會發生這件事就好。
Step 2 編譯成 Bytecode
接下來,剛才解析好的 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 版本的意思。
所以,如果你曾經在你的 Python 專案看到像是 __pycache__
這樣的目錄的話,這些就是編譯過的 Bytecode 啦。當再次執行同樣的 Python 程式時,Python 直譯器會先檢查是否有對應的 .pyc
檔案以及是否需要重新再編譯一次,如果 .pyc
已經存在而且不用重新編譯就會直接載入這些 Bytecode,交由 Python 虛擬機器(Virtual Machine)解譯、執行。
把 AST 編譯成 Bytecode 的原始碼在 CPython 專案裡的位置是 Python/compile.c
,有興趣的話可以追一下這個檔案裡的 _PyAST_Compile()
函數,這個函數會把 AST 轉換成可以執行的 Code Object。我們可以試著用 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>)
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>:
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 原始碼是這段:
static PyObject *
builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
PyObject *kwnames)
{
PyObject *func, *name, *winner, *prep;
// ... 略 ...
return cls;
}
這個函數比較長一點,我先挑一小段出來:
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 在後面會有專門一整個章節介紹 class 與 Metaclass 的關係。
這個 builtin___build_class__()
函數裡還有很多東西值得介紹的,不過同樣也等後續介紹到物件導向的章節再來說明。簡單的說,這個函數會回傳一個類別,但仔細看就會發現,所謂的類別也就是一個 PyObject
,這也證明了在 Python 裡類別也只是一種物件而已。
類別建立完成,再來就是準備建立實體了。
step 3 建立實體
在剛才的 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)」這個類別。在 Python 的世界有很多的頭尾有兩個底線的魔術方法,__call__
也是其中一個,只要這個物件身上有 __call__
這個魔術方法,這顆物件就能被「呼叫」。
雖然我自己寫的 Cat
類別並沒有實作 __call__
這個魔術方法,但是建立 Cat
類別的 Metaclass 是 type
類別,它身上有這個方法,所以當「呼叫」或「執行」Cat
類別的時候,看的就是 type
類別的 tp_call
成員變數:
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
的函數,再繼續看看這個函數對應到的實作內容:
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__
方法是對的起來的。
小結
如果我們自己寫了一個類別並且用它來建立實體的過程,會經過以下流程:
編譯階段:
- 把原始碼轉換成 Token。
- 把 Token 轉換成 AST。
- 把 AST 編譯成 Bytecode。
執行階段:
- 使用小括號
()
呼叫類別,也就是執行成員方法tp_call()
。 type
類別預設的tp_call()
會呼叫tp_new()
函數來建立物件。- 接著呼叫物件的
tp_init()
函數來初始化物件。
順帶一提,type
這傢伙很有趣,大家平常可能常會用 type("hello kitty")
這樣的寫法來取得 "hello kitty"
這個字串的類別名稱,不少人會以為它就是一般的函數,但其實它是個物件也是個類別。在剛才介紹到的 type_call()
函數裡有 這樣寫到:
/* 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'>
的原因。