跳至主要内容

類別與它們的產地

為你自己學 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 原始碼。

決定 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;
}

如果沒有設定 meta 的話,就會看看有沒有指定的上層類別,如果沒有上層類別,就用 PyType_Type 來當做 meta,這個在 Python 其實也就是 type 類別。如果有上層類別的話呢?因為 Python 的類別可以同時有多個上層類別,所以會拿上層類別的第一個類別的 meta 來當做這個類別的 meta,這裡的 Py_TYPE(base0) 會取得 base0ob_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。到這裡答案就揭曉了:

  • 所有的類別在建立的過程,都是透過呼叫這個類別的 metaclass 而建立的。
  • 如果有指定 metaclass 的話,就會用指定的 metaclass 來建立類別。
  • 如果沒有指定 metaclass 就得看情況:
    • 有上層類別,就會用第一個上層類別的 metaclass 來當 metaclass。
    • 沒有上層類別,就會用內建的 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 嗎?我教你啊 :)

想要成為軟體工程師嗎?這不是條輕鬆的路,除了興趣之外,還需要足夠的決心、設定目標並持續學習,我們的ASTROCamp 軟體工程師培訓營提供專業的前後端課程培訓,幫助你在最短時間內建立正確且扎實的軟體開發技能,有興趣而且不怕吃苦的話不妨來試試看!