深入理解Python虛擬機器:super超級魔法的背後原理

一無是處的研究僧發表於2023-10-12

深入理解Python虛擬機器:super超級魔法的背後原理

在本篇文章中,我們將深入探討Python中的super類的使用和內部工作原理。super類作為Python虛擬機器中強大的功能之一,super 可以說是 Python 物件系統基石,他可以幫助我們更靈活地使用繼承和方法呼叫。

super類的使用

在 Python 中,我們經常使用繼承來構建類的層次結構。當子類繼承了父類的屬性和方法時,有時我們需要在子類中呼叫父類的方法或屬性。這就是super類的用武之地。

super函式的一般用法是在子類中呼叫父類的方法,格式為super().method()。這樣可以方便地使用父類的實現,並在子類中新增自己的特定行為。

下面是一個示例程式碼,演示了super函式的使用:

class Parent:
    def __init__(self, name):
        self.name = name
    
    def say_hello(self):
        print(f"Hello, I'm {self.name}")

class Child(Parent):
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age
    
    def say_hello(self):
        super().say_hello()
        print(f"I'm {self.name} and I'm {self.age} years old")

child = Child("Alice", 10)
child.say_hello()

輸出結果為:

Hello, I'm Alice
I'm Alice and I'm 10 years old

在上述示例中,Child類繼承自Parent類。在Child類的建構函式中,我們使用super().__init__(name)來呼叫父類Parent的建構函式,以便在子類中初始化父類的屬性。在say_hello方法中,我們使用super().say_hello()呼叫父類Parentsay_hello方法,並在子類中新增了額外的輸出。

除了呼叫父類的方法,super函式還可以用於訪問父類的屬性。例如,super().attribute可以用來獲取父類的屬性值。

super類的工作原理

Super 設計的目的

要理解super類的工作原理,我們需要了解Python中的多重繼承和方法解析順序(Method Resolution Order,MRO)。多繼承是指一個類可以同時繼承多個父類。在Python中,每個類都有一個內建屬性__mro__,它記錄了方法解析順序。MRO是根據C3線性化演演算法生成的,它決定了在多重繼承中呼叫方法的順序。當物件進行方法呼叫的時候,就會從類的 mro 當中的第一個類開始尋找,直到最後一個類為止,當第一次發現對應的類有相應的方法時就進行返回就呼叫這個類的這個方法。關於 C3 演演算法和 mro 的細節可以參考文章 深入理解 python 虛擬機器:多繼承與 mro

Super 類的的簽名為 class super(type, object_or_type=None),這個類返回的是一個 super 物件,也是一個代理物件,當使用這個物件進行方法呼叫的時候,這個呼叫會轉發給 type 父類或同級類。object_or_type 引數的作用是用於確定要搜尋的方法解析順序(也就是透過object_or_type得到具體的 mro),對於方法的搜尋從 type 後面的類開始。

例如,如果 的 object_or_type 的 mro 是 D -> B -> C -> A -> object 並且type的值是 B ,則進行方法搜尋的順序為C -> A -> object ,因為搜尋是從 type 的下一個類開始的。

下面我們使用一個例子來實際體驗一下:

class A:

	def __init__(self):
		super().__init__()

	def method(self):
		print("In method of A")


class B(A):

	def __init__(self):
		super().__init__()

	def method(self):
		print("In method of B")


class C(B):

	def __init__(self):
		super().__init__()

	def method(self):
		print("In method of C")


if __name__ == '__main__':
	print(C.__mro__)
	obj = C()
	s = super(C, obj)
	s.method()
	s = super(B, obj)
	s.method()

上面的程式輸出結果為:

(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
In method of B
In method of A

在上面的程式碼當中繼承順序為,C 繼承 B,B 繼承 A,C 的 mro 為,(C, B, A, object),super(C, obj) 表示從 C 的下一個類開始搜尋,因此具體的搜尋順序為 ( B, A, object),因此此時呼叫 method 方法的時候,會呼叫 B 的 method 方法,super(B, obj) 表示從 B 的下一個類開始搜尋,因此搜尋順序為 (A, object),因此此時呼叫的是 A 的 method 方法。

Super 和棧幀的關係

在上一小節當中我們在使用 super 進行測試的時候,都是給了 super 兩個引數,但是需要注意的是我們在一個類的 __init__方法當中並沒有給 super 任何引數,那麼他是如何找到 super 需要的兩個引數呢?

這其中的魔法就是在 Super 類物件的初始化會獲取當前棧幀的第一個引數物件,這個就是對應上面的 object_or_type 引數,type 就是區域性變數表當中的一個引數 __class__,我們可以透過檢視類方法的區域性變數去驗證這一點:

import inspect


class A(object):

	def __init__(self):
		super().__init__()
		print(inspect.currentframe().f_locals)

	def bar(self):
		pass

	def foo(self):
		pass


class Demo(A):

	def __init__(self):
		super().__init__()
		print(inspect.currentframe().f_locals)

	def bar(self):
		super().bar()
		print(inspect.currentframe().f_locals)

	def foo(self):
		print(inspect.currentframe().f_locals)


if __name__ == '__main__':
	demo = Demo()
	demo.bar()
	demo.foo()

上面的程式碼輸出結果為:

{'self': <__main__.Demo object at 0x103059040>, '__class__': <class '__main__.A'>}
{'self': <__main__.Demo object at 0x103059040>, '__class__': <class '__main__.Demo'>}
{'self': <__main__.Demo object at 0x103059040>, '__class__': <class '__main__.Demo'>}
{'self': <__main__.Demo object at 0x103059040>}

從上面的例子我們可以看到當我們進行方法呼叫且方法當中有 super 的使用時,棧幀的區域性變數表當中會多一個欄位 __class__,這個欄位表示對應的類,比如在 Demo 類當中,這個欄位就是 Demo,在類 A 當中這個欄位就是 A 。為什麼要進行這樣的處理呢,這是因為需要呼叫相應位置類的父類方法,因此所有的使用 super 的位置的 type 都必須是所在類。而在前面我們已經說明瞭object_or_type 表示的是棧幀當中的第一個引數,也就是物件 self,這一點從上面的區域性變數表也可以看出來,透過這個物件我們可以知道物件本身的 mro 序列了。在 super 得到兩個引數之後,也就能夠實現對應的功能了。

CPython的實現

在本小節當中我們來仔細看一下 CPython 內部是如何實現 super 類的,首先來看一下他的 __init__ 方法(刪除了error checking 程式碼):

static int
super_init(PyObject *self, PyObject *args, PyObject *kwds)
{
    superobject *su = (superobject *)self;
    PyTypeObject *type = NULL; // 表示從哪個類的後面開始查詢,含義和 上文當中的 type 一樣
    PyObject *obj = NULL; // 表示傳遞過來的物件
    PyTypeObject *obj_type = NULL; // 表示物件 obj 的型別
    // 獲取 super 的兩個引數 type 和 object_or_type
    if (!PyArg_ParseTuple(args, "|O!O:super", &PyType_Type, &type, &obj))
        return -1;

    if (type == NULL) {
        /* Call super(), without args -- fill in from __class__
           and first local variable on the stack. */
        PyFrameObject *f;
        PyCodeObject *co;
        Py_ssize_t i, n;
        f = _PyThreadState_GET()->frame; // 得到當前棧幀
        // 棧幀的第一個引數列示物件
        obj = f->f_localsplus[0];
        if (obj == NULL && co->co_cell2arg) {
            /* The first argument might be a cell. */
            n = PyTuple_GET_SIZE(co->co_cellvars);
            for (i = 0; i < n; i++) {
                if (co->co_cell2arg[i] == 0) {
                    PyObject *cell = f->f_localsplus[co->co_nlocals + i];
                    assert(PyCell_Check(cell));
                    obj = PyCell_GET(cell);
                    break;
                }
            }
        }
        if (co->co_freevars == NULL)
            n = 0;
        else {
            assert(PyTuple_Check(co->co_freevars));
            n = PyTuple_GET_SIZE(co->co_freevars);
        }
        // 下面的程式碼表示獲取 type 物件,也就是從區域性變數表當中獲取到 __class__ 
        for (i = 0; i < n; i++) {
            PyObject *name = PyTuple_GET_ITEM(co->co_freevars, i);
            assert(PyUnicode_Check(name));
            if (_PyUnicode_EqualToASCIIId(name, &PyId___class__)) {
                Py_ssize_t index = co->co_nlocals +
                    PyTuple_GET_SIZE(co->co_cellvars) + i;
                PyObject *cell = f->f_localsplus[index];
                type = (PyTypeObject *) PyCell_GET(cell);
                break;
            }
        }
    }

    if (obj == Py_None)
        obj = NULL;
    if (obj != NULL) {
        // 這個函式是用於獲取 obj 的 type
        obj_type = supercheck(type, obj);
        if (obj_type == NULL)
            return -1;
        Py_INCREF(obj);
    }
    return 0;
}

在上面的程式碼執行完成之後就得到了一個 super 物件,之後在進行函式呼叫的時候就會將對應類的方法和物件 obj 繫結成一個方法物件返回,然後在進行方法呼叫的時候就能夠成功呼叫了。

class Demo:

	def __init__(self):
		print(super().__init__)


if __name__ == '__main__':
	Demo()

輸出結果:

<method-wrapper '__init__' of Demo object at 0x100584070>

總結

super 是 Python 物件導向程式設計當中非常重要的一部分內容,在本篇文章當中詳細介紹了 super 內部的工作原理和 CPython 內部部分原始碼分析了 super 的具體實現。在 Python 當中 super 的使用方式分為兩種一種是可以直接使用引數,另外一種是在類的方法當中不使用引數,後者的實現稍微複雜一點,他會從當前棧幀和區域性變數表當中分別取出類物件和類,作為 super 的引數,從而實現 super 的功能。


本篇文章是深入理解 python 虛擬機器系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演演算法與資料結構)知識。

相關文章