類別與它們的產地
大家講到物件導向程式設計,大概就會講到類別(Class)這個東西。類別可以建立「實體(Instance)」,也就是我們常說的物件。但在一切都是物件的 Python 世界,類別也是物件。如果類別是物件,那麼這個類別物件本身又是誰哪個類別建立的?我們就用這個簡單的類別當做範例:
class Cat:
def __init__(self, name):
self.name = name
def meow(self):
print(f"Hello, {self.name}")
kitty = Cat("Kitty")
kitty.meow()
這個章節就要從 CPython 的原始碼來看看這段程式碼執行的過程中發生什麼事,以及這個類別是怎麼建立的。
建立類別
既然類別也是物件,根據我們前面章節學到的內容,大概會猜 CPython 裡可能也有個 PyClassObject
這樣的東西,但會發現根本沒這個東西。沒關係,我們從 Bytecode 下手,看看到底 class
語法是怎麼建立類別的:
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)
// ... 略 ...
這裡唯一沒看過的指令是 LOAD_BUILD_CLASS
,從指令名字大概猜的出來是在建立類別,來看看它做什麼事:
inst(LOAD_BUILD_CLASS, ( -- bc)) {
if (PyDict_CheckExact(BUILTINS())) {
bc = _PyDict_GetItemWithError(BUILTINS(),
&_Py_ID(__build_class__));
// ... 省略錯誤處理 ...
Py_INCREF(bc);
}
else {
bc = PyObject_GetItem(BUILTINS(), &_Py_ID(__build_class__));
// ... 省略錯誤處理 ...
}
}
這會檢查 BUILDINS()
裡是不是一個字典,如果是就使用 _PyDict_GetItemWithError()
,否則就呼叫 PyObject_GetItem()
。PyObject_GetItem()
這個函數我們之前看過,它的流程稍微多一點,所以效能上會比 _PyDict_GetItemWithError()
差一點。
但是,BUILDINS()
什麼時候不會是字典?大部份時候都是,除非我們自己手賤去改了 __builtins__
這個變數。__builtins__
是 Python 內建的模組,裡面有很多 Python 內建的函數,但如果要手動改它也不是不行:
>>> __builtins__
<module 'builtins' (built-in)>
>>> __builtins__ = "Hello Kitty"
>>> __builtins__
'Hello Kitty'
這樣 BUILDINS()
就不是字典了,只是這可能會讓程式出現奇怪甚至是錯誤的結果,除非你知道自己在幹嘛,不然請不要這樣做。那麼 __build_class__
是什麼?這其實是個 Python 內建的函數:
>>> __build_class__
<built-in function __build_class__>
也就是說,其實 LOAD_BUILD_CLASS
這個指令就是把 __build_class__
這個函數抓出來,待會要再用它建立類別。這個函數可能大家比較少用到,我用它來建立一個簡單的 Cat
類別給大家看看。首先我先定義一個函數:
def cat_body():
def __init__(self, name):
self.name = name
def meow(self):
print(f"Hello, {self.name}")
return locals()
在這個函數裡定義了 __init__()
以及 meow()
兩個函數,因為這兩個函數都算是 cat_body()
函數的區域變數,所以最後透過 locals()
函數回傳的區域變數也包括這兩個函數。接著我們用 __build_class__()
這個函數來建立類別:
MyCat = __build_class__(cat_body, "Cat")
kitty = MyCat("Kitty")
kitty.meow()
這樣就能建立一個 Cat
類別了。在 Python 的世界裡,class
就只是個語法糖,背後就是透過 __build_class__()
這個內建函數在做事的...
幕後黑手!
那麼,這個內建函數 __build_class__()
是怎麼定義的:
static PyObject *
builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
PyObject *kwnames)
{
// ... 略 ...
}
這大概有 150 行左右的函數,我們來一段一段看:
if (nargs < 2) {
PyErr_SetString(PyExc_TypeError,
"__build_class__: not enough arguments");
return NULL;
}
func = args[0]; /* Better be callable */
if (!PyFunction_Check(func)) {
PyErr_SetString(PyExc_TypeError,
"__build_class__: func must be a function");
return NULL;
}
name = args[1];
if (!PyUnicode_Check(name)) {
PyErr_SetString(PyExc_TypeError,
"__build_class__: name is not a string");
return NULL;
}
剛才我們在用 __build_class__()
函數的時候,第一個跟第二個參數都是一定要給的,而且第一個是函數,第二個是字串,所以這段就是在做這些檢查。接下來:
// ... 略 ...
orig_bases = _PyTuple_FromArray(args + 2, nargs - 2);
// ... 略 ...
bases = update_bases(orig_bases, args + 2, nargs - 2);
為什麼這裡跳掉前 2 個參數?因為第一個是函數,第二個是字串,第三個參數開始的所有參數就可能會是它的上層類別。接下來的 update_bases()
函數是用來針對這些上層類別做一些特別的處理,這細節跟繼承有關,在下個章節就會來跟大家介紹。再往下看:
meta = _PyDict_GetItemWithError(mkw, &_Py_ID(metaclass));
這會試著用關鍵字引數裡找看看有沒有 "metaclass"
這個 Key,如果有的話就把它抓出來。這裡的 _Py_ID(metaclass)
其實就是一個字串而已,但為什麼這樣寫?我們之前有介紹過 CPython 為了效能考量,會把一些常用的字串先編譯到直譯器裡,"metaclass"
就是其中之一,所以這樣的寫法可以直接拿那個編譯好的字串來用,不用重新建立一個新的字串,效能比較好。如果想知道還有哪些字串也一起被編譯進直譯器,可翻閱 Tools/build/generate_global_objects.py
原始碼。
決定 Metaclass
接下來這段就是重點了:
if (meta == NULL) {
if (PyTuple_GET_SIZE(bases) == 0) {
meta = (PyObject *) (&PyType_Type);
}
else {
PyObject *base0 = PyTuple_GET_ITEM(bases, 0);
meta = (PyObject *)Py_TYPE(base0);
}
Py_INCREF(meta);
isclass = 1;
}