本文同步刊載於 「為你自己學 Python - 類別與它們的產地)」
大家講到物件導向程式設計,大概就會講到類別(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
,從指令名字大概猜的出來是在建立類別,來看看它做什麼事:
// 檔案:Python/bytecodes.c
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」的物件導向程式設計 - 進階篇也曾介紹過,其實在 Python 的世界裡,class
就只是個語法糖,背後就是透過 __build_class__()
這個內建函數在做事的。
那麼,這個內建函數 __build_class__()
是怎麼定義的:
// 檔案:Python/bltinmodule.c
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
原始碼。
接下來這段就是重點了:
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;
}
如果沒有設定 meta
的話,就會看看有沒有指定的上層類別,如果沒有上層類別,就用 PyType_Type
來當做 meta
,這個在 Python 其實也就是 type
類別。如果有上層類別的話呢?因為 Python 的類別可以同時有多個上層類別,所以會拿上層類別的第一個類別的 meta 來當做這個類別的 meta,這裡的 Py_TYPE(base0)
會取得 base0
的 ob_type
成員,其實就是取得它的 meta class 的意思。再接著往下看:
if (isclass) {
winner = (PyObject *)_PyType_CalculateMetaclass((PyTypeObject *)meta,
bases);
if (winner == NULL) {
goto error;
}
if (winner != meta) {
Py_SETREF(meta, Py_NewRef(winner));
}
}
這裡透過 _PyType_CalculateMetaclass()
函數計算出一個「贏家(Winner)」出來。為什麼需要算誰贏誰輸?因為在 Python 有多重繼承的設計,所以一個類別可能會同時有多個上層類別情況發生,這時候就要有一套遊戲規則把原本的 meta
跟其它的上層類別 bases
丟進去算一下誰是 Metaclass 是誰,至於這個計算方法,我們在下個章節就會說明。
再接著往下看:
if (_PyObject_LookupAttr(meta, &_Py_ID(__prepare__), &prep) < 0) {
ns = NULL;
}
else if (prep == NULL) {
ns = PyDict_New();
}
else {
PyObject *pargs[2] = {name, bases};
ns = PyObject_VectorcallDict(prep, pargs, 2, mkw);
Py_DECREF(prep);
}
這段程式碼是用來準備 namespace 用的,如果在 meta
裡有定義了 __prepare__
這個方法的話,就會呼叫這個方法來準備 namespace,否則就用一個空的字典當作 namespace。這個 __prepare__
在 Python 可以怎麼玩的?來寫個範例:
class MetaCat(type):
def __prepare__(name, bases):
print(f"Hello Meta! {name} {bases}")
return {"SPECIAL_NAME": "Hello Kitty"}
class Cat(metaclass=MetaCat):
pass
這樣一來 Cat
類別以及由它建立出來的實體都可以有 .SPECIAL_NAME
這個屬性。想要知道更多這個屬性的細節,可參考官網以及PEP-3115的介紹。
回到原本的程式碼,這個函數即將進入尾聲:
cell = _PyEval_Vector(tstate, (PyFunctionObject *)func, ns, NULL, 0, NULL);
if (cell != NULL) {
if (bases != orig_bases) {
if (PyMapping_SetItemString(ns, "__orig_bases__", orig_bases) < 0) {
goto error;
}
}
PyObject *margs[3] = {name, bases, ns};
cls = PyObject_VectorcallDict(meta, margs, 3, mkw);
if (cls != NULL && PyType_Check(cls) && PyCell_Check(cell)) {
PyObject *cell_cls = PyCell_GET(cell);
if (cell_cls != cls) {
if (cell_cls == NULL) {
const char *msg =
"__class__ not set defining %.200R as %.200R. "
"Was __classcell__ propagated to type.__new__?";
PyErr_Format(PyExc_RuntimeError, msg, name, cls);
} else {
const char *msg =
"__class__ set to %.200R defining %.200R as %.200R";
PyErr_Format(PyExc_TypeError, msg, cell_cls, name, cls);
}
Py_SETREF(cls, NULL);
goto error;
}
}
}
這個 cell
我們在上個章節就看過它了,如果 cell
不是空的,就準備來建立類別了,重點在這行:
cls = PyObject_VectorcallDict(meta, margs, 3, mkw);
這行就是呼叫 meta
來建立物件 cls
。到這裡答案就揭曉了:
type
來當做 metaclass。綜合以上內容,所有的類別不管是自己指定的還是預設的,都會有一個 metaclass。因為內建的類別的 metaclass 都是 type
,除了自己手動指定 metaclass 的類別之外,在 Python 裡的其它類別可以說都是 type
類別建立的。
這也是為什麼我們試著用 type()
來印出所有的類別的時候,不管是內建的還是自己寫的:
>>> type(object)
<class 'type'>
>>> type(int)
<class 'type'>
>>> type(str)
<class 'type'>
>>> class Dog:
... pass
>>> type(Dog)
<class 'type'>
答案都是 <class 'type'>
,但如果是自己指定 metaclass 的:
>>> type(Cat)
<class '__main__.MetaCat'>
就會是自己指定的 metaclass。
如果你再繼續看下去,會發現 type
類別自己本身的 metaclass 本身也是 type
:
>>> type(type)
<class 'type'>
但這怎麼做到的?自己怎麼生出自己來?來看看 PyType_Type
的定義:
// 檔案:Objects/typeobject.c
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
sizeof(PyHeapTypeObject), /* tp_basicsize */
// ... 略 ...
};
這裡可以看到為什麼會印出 "type"
字樣,在 PyVarObject_HEAD_INIT()
這個巨集就是決定先有雞還是先有蛋的關鍵了:
// 檔案:Include/object.h
#define PyObject_HEAD_INIT(type) \
{ \
_PyObject_EXTRA_INIT \
{ 1 }, \
(type) \
},
這段巨集在做的事,其實就是把傳進來的參數,也就是 PyType_Type
指定給自己的 ob_type
成員變數,這樣就可以讓 PyType_Type
這個類別的 metaclass 是 type
了。
是說為什麼要這樣設計?如果不這麼設計的話,「所有類別的 metaclass 都是 type
」這個故事就講不下去啦 :)
本文同步刊載於 「為你自己學 Python - 類別與它們的產地)」