《深度剖析CPython解釋器》18. Python類機制的深度解析(第二部分): 類的多繼承與屬性查找


楔子

這次我們來看一下Python中類的繼承與屬性查找機制,我們說Python是支持類的多繼承的,在查找屬性的時候會沿着繼承鏈不斷找下去。那么下面我們就來深入地考察一下類的繼承與屬性查找在底層是如何實現的。

深入class

我們知道Python里面有很多以雙下划線開頭、雙下划線結尾的方法,我們稱之為魔法方法。Python中的每一個操作符,都被抽象成了一個魔法方法。比如整數3,整數可以相減,這就代表int這個類里面肯定定義了__sub__函數。字符串不能相減,代表str這個類里面沒有__sub__函數;而整數和字符串都可以執行加法操作,顯然int、str內部都定義了__add__函數。

class MyInt(int):

    def __sub__(self, other):
        return int.__sub__(self, other) * 3


a = MyInt(4)
b = MyInt(1)
print(a - b)  # 9

我們自己實現了一個類,繼承自int。當我執行a - b的時候,肯定執行對應的__sub__方法,然后調用int的__sub__方法,得到結果之后再乘上3,邏輯上沒有問題。但是問題來了,首先調用int.__sub__的時候,我們知道Python肯定是調用long_as_number中的long_sub指向的函數,這些在之前已經說過了,但我想問的是int.__sub__(self, other)里面的參數類型顯然都應該是int,但是我們傳遞的是MyInt,那么Python虛擬機是怎么做的呢?

目前帶着這些疑問,先來看一張草圖,我們后面會一點一點揭開:

圖中的"__sub__"對應的value並不是一個直接指向long_sub函數的指針,而是一個別的什么東西,對其進行調用,調用的結果指向long_sub函數。至於這個東西是什么,以及具體細節,我們后面會詳細說。

另外我們知道,一個對象能否被調用,取決於它的類型對象中是否定義了__call__函數。因此:所謂調用,就是執行類型對象中的tp_call指向的函數。

class A:

    def __call__(self, *args, **kwargs):
        return "我是CALL,我被尻了"

    
a = A()
print(a())  # 我是CALL,我被尻了

在Python底層,實際上是通過一個PyObject_Call函數對實例對象a進行操作。

a = 1
a()
# TypeError: 'int' object is not callable

我們看到一個整數對象是不可調用的,這顯然意味着int這個類里面沒有__call__函數,換言之PyLongObject結構體對應的ob_type域里面的tp_call為NULL。

# 但是我們通過反射打印的時候,發現int是有__call__函數的啊
print(hasattr(int, "__call__"))  # True

# 但其實這個__call__不是int里面的,而是type的
print("__call__" in dir(int))  # False
print("__call__" in dir(type))  # True
print(int.__call__)  # <method-wrapper '__call__' of type object at 0x00007FFAE22C0D10>

# hasattr和類的屬性查找一樣,如果找不到會自動到對應的類型對象里面去找
# int的類型是type, 而type里面有__call__, 因此即便int里面沒有, hasattr(int, "__call__")依舊是True
a1 = int("123")
a2 = int.__call__("123")
a3 = type.__call__(int, "123")
# 以上三者的本質是一樣的
print(a1, a2, a3)  # 123 123 123

# 之前說過, 當一個對象加上括號的時候, 本質上調用其類型對象里面的__call__函數
# a = 3
# 那么a()就相當於調用int里面的__call__函數,但是int里面沒有,就直接報錯了
# 可能這里就有人問了, 難道不會到type里面找嗎?答案是不會的,因為type是元類, 是用來生成類的
# 如果還能到type里面找, 那么調用type的__call__生成的結果到底算什么呢?是類對象?可它又明明是實例對象加上括號調用的。顯然這樣就亂套了
# 因此實例對象找不到, 會到類對象里面找, 如果類對象再找不到, 就不會再去元類里面找了, 而是會去父類里面找
class A:
    def __call__(self, *args, **kwargs):
        print(self)
        return "我被尻了"


class B(A):
    pass


class C(B):
    pass


c = C()
print(c())
"""
<__main__.C object at 0x000002282F3D9B80>
我被尻了
"""
# 此時我們看到, 給C的實例對象加括號的時候, C里面沒有__call__函數, 這個時候是不會到元類里面找的
# 還是之前的結論,實例對象找不到屬性,會去類對象里面找,然而即便此時類對象里面也沒有,也不會到元類type里面找,這時候就看父類了

# 我們看對於我們上面的例子,給C的實例對象加括號的時候,會執行C這個類里面的__call__
# 但是它沒有,所以找不到。然而它繼承的父類里面有__call__
# 因此會執行繼承的父類的__call__方法, 並且里面的self還是C的實例對象

因此一個對象的屬性查找,我們可以得到如下規律:首先從對象本身進行查找,沒有的話會從該對象的類型對象中進行查找,還沒有的話就從類型對象所繼承的父類中進行查找。

class A:
    def __call__(self, *args, **kwargs):
        print(self)
        return "我被尻了"


class B(A):
    pass


class C(B):
    pass


c = C()
print(c())

還是以這段代碼為例:當調用類型對象C的時候,本質上是執行類型對象C的類型對象(type)里面的__call__函數。當調用實例對象c的時候,本質上是執行類型對象C里面的__call__函數,但是C里面沒有,這個時候怎么做?顯然是沿着繼承鏈進行屬性查找,去找C繼承的類里面的__call__函數。

可能有人好奇,為什么沒有object?答案是object內部沒有__call__函數,所以object.__call__實際上就是type.__call__。

print(object.__call__)  # <method-wrapper '__call__' of type object at 0x00007FFD0A896B50>

因為object的類型是type,所以調用object的時候,實際上執行的是type.__call__(object)。

所以所有的類對象都是可以調用的,因為type是它們的類型對象,而type內部是有__call__函數的。但是默認情況下實例對象是不可調用的,如果實例對象的類型對象、以及該類型對象所繼承的類中沒有定義__call__函數的話,因為沿着繼承鏈查找的時候,會挨個進行搜索,當搜索到object時發現還沒有__call__函數的話,那么就報錯了。

所以一個整數對象是不可調用的,但是我們發現這並不是在編譯的時候就能夠檢測出來的錯誤,而是在運行時才能檢測出來、會在運行時通過函數 PyObject_CallFunctionObjArgs 確定。所以a = 1;a()明明會報錯,但Python還是成功編譯了。

為什么會是這樣呢?我們知道一個對象對應的類型對象都會有tp_dict這個域,這個域指向一個PyDictObject,表示這個對象支持哪些操作,而這個PyDictObject對象必須要在運行時動態構建。所以都說Python效率慢,一個原因是所有對象都分配在堆上,還有一個原因就是一個對象很多屬性或者操作、甚至是該對象是什么類型都需要在運行時動態構建,從而也就造成了Python運行時效率不高。

而且我們發現,像int、str、dict等內建對象可以直接使用。這是因為Python解釋器在啟動時,會對這些內建對象進行初始化的動作。這個初始化的動作會動態地在這些內建對象對應的PyTypeObject中填充一些重要的東西,其中當然也包括填充tp_dict,從而讓這些內建對象具備生成實例對象的能力。這個對內建對象進行初始化的動作就從函數 PyType_Ready 拉開序幕。

Python底層通過調用函數 PyType_Ready 對內建對象進行初始化,實際上, PyType_Ready 不僅僅是處理內建對象,還會處理class對象,並且 PyType_Ready 對於內建對象和class對象的作用還不同。比如PyList_Type,它在底層是已經被定義好了的,所以在解釋器啟動的時候就直接創建,並且是全局對象。只不過我們說它還是不夠完善,還需要再打磨一下,而這一步就交給了 PyType_Ready 。

但是對於我們自定義的類就不同了,我們說內建的類在底層都是定義好了的,隨着解釋器啟動的時候就已經創建了,已經具備了絕大部分功能,然后再交給 PyType_Ready 完善一下,內建的類就形成了;但是對於我們自定義的類來說, PyType_Ready 做的工作只是很小的一部分,因為我們使用class定義的類、假設是class A,Python一開始是並不知道的。Python解釋器在啟動的時候,不可能直接就創建一個PyA_Type出來,因此對於我們自定義的類來說,需要在解釋執行的時候進行申請內存、創建、初始化整個動作序列等等一系列步驟。

下面我們就以Python中的type類型對象入手,因為它比較特殊。Python中的type在底層對應PyType_Type,我們說Python中type是int、str、dict、object、type等內建對象的元類。但是在底層,這些所有的內建類型都是一個PyTypeObject對象。

  • int: PyLong_Type
  • str: PyUnicode_Type
  • tuple: PyTuple_Type
  • dict: PyDict_Type
  • type: PyType_Type

從名字也能看出來規律,這些內建對象在Python底層中,都是一個PyTypeObject對象、或者說一個PyTypeObject結構體實例。盡管在Python中說type是所有類對象(所有內建對象+class對象)的元類,但是在Python底層它們都是同一個類型、也就是同一個結構體的不同實例。

處理基類和type信息

我們來看一下 PyType_Ready ,它位於 Objects / typeobject.c 中。

int
PyType_Ready(PyTypeObject *type)
{	
    //這里的參數顯然是類型對象, 以<class 'type'>為例
    
    //__dict__和__bases__, 因為可以繼承多個類, 所以是bases, 當然不用想這些基類也都是PyTypeObject對象
    PyObject *dict, *bases;
    
    //還是繼承的基類,顯然這個是object,對應PyBaseObject_Type,因為py3中,所有的類都是默認繼承的
    PyTypeObject *base;
    Py_ssize_t i, n;
    
    //......
    //......
    
    //獲取類型對象中tp_base域指定的基類
    base = type->tp_base;
    if (base == NULL && type != &PyBaseObject_Type) {
        //如果基類為空、並且該類本身不是object, 那么將該類的基類設置為object、即PyBaseObject_Type
        //所以我們之前看一些類型對象的底層定義的時候, 發現源碼中tp_base域對應的是0, 因為tp_base是在這里進行設置的
        base = type->tp_base = &PyBaseObject_Type;
        Py_INCREF(base);
    }

    //如果基類不是NULL, 但是基類的屬性字典是NULL
    if (base != NULL && base->tp_dict == NULL) {
        //那么對基類進行初始化, 所以這里是一個是一個遞歸調用
        if (PyType_Ready(base) < 0)
            goto error;
    }

    //如果該類型對象的ob_type為空NULL但是基類不為NULL, 那么將該類型對象的ob_type設置為基類的ob_type
    //為什么要做這一步, 我們后面會詳細說
    //但其實base != NULL是沒必要的, 因為只有當類型為PyBaseObject_type時, base才為NULL
    if (Py_TYPE(type) == NULL && base != NULL)
        Py_TYPE(type) = Py_TYPE(base);

    //這里是設置__bases__, 后面說
    bases = type->tp_bases;
    if (bases == NULL) {
        if (base == NULL)
            bases = PyTuple_New(0);
        else
            bases = PyTuple_Pack(1, base);
        if (bases == NULL)
            goto error;
        type->tp_bases = bases;
    }

    //構造屬性字典, 后面說
    dict = type->tp_dict;
    if (dict == NULL) {
        dict = PyDict_New();
        if (dict == NULL)
            goto error;
        type->tp_dict = dict;
    }
    //......
}

對於指定了tb_base的類對象,當然就使用指定的基類,而對於沒有指定tp_base的類對象,Python將為其指定一個默認的基類: PyBaseObject_Type ,當然這個東西就是Python中的object。現在我們看到PyType_Type的tp_base指向了PyBaseObject_Type,這在Python中體現的就是type繼承自object、或者說object是type的父類。但是所有的類底層對應的結構體的ob_type域又都指向了PyType_Type,包括object,因此我們又說type是包括object在內的所有類的類(元類)

在獲得了基類之后,就會判斷基類是否被初始化,如果沒有,則需要先對基類進行初始化。可以看到, 判斷初始化是否完成的條件是base->tp_dict是否為NULL,這符合之前的描述。對於內建對象的初始化來說,在Python解釋器啟動的時候,就已經作為全局對象存在了,剩下的就是小小的完善一下,比如對tp_dict進行填充。

然后設置ob_type信息,實際上這個ob_type就是__class__返回的信息。首先 PyType_Ready 函數里面接收的是一個PyTypeObject對象,我們知道這個在Python中就是類對象。因此這里是設置這些類對象的ob_type,那么對應的ob_type顯然就是元類metaclass,我們自然會想象到Python中的type。但是我們發現Py_TYPE(type) = Py_TYPE(base);這一行代碼是把父類的ob_type設置成了當前類的ob_type,那么這一步的意義何在呢?我們使用Python來演示一下。

class MyType(type):
    pass


class A(metaclass=MyType):
    pass


class B(A):
    pass


print(type(A))  # <class '__main__.MyType'>
print(type(B))  # <class '__main__.MyType'>

我們看到B繼承了A,而A的類型是MyType,那么B的類型也成了MyType。也就是說A類是由XX生成的,那么B在繼承A的時候,B也會由XX生成,所以源碼中的那一步就是用來做這件事情的。另外,這里之所以用XX代替,是因為Python中不僅僅type可以是元類,那些繼承了type的子類也可以是元類。

而且如果你熟悉flask的話,你會發現flask源碼里面就有類似於這樣的操作:

class MyType(type):

    def __new__(mcs, name, bases, attrs):
        """
        關於第一個參數我們需要說一下, 對於一般的類來說這里應該是cls
        但我們這里是元類, 所以用mcs代替
        """
        # 我們額外設置一些屬性吧, 關於元類我們后續會介紹
        # 不過個人覺得既然要學習解釋器, 那么首先至少應該在Python層面上知道用法
        # 盡管不知道底層實現, 但至少使用方法應該知道
        if name.startswith("G"):
            # 如果類名以G開頭, 那么就設置一些屬性吧
            attrs.update({"name": "夏色祭"})
        return super().__new__(mcs, name, bases, attrs)


def with_metaclass(meta, bases=(object, )):
    return meta("", bases, {})


class Girl(with_metaclass(MyType, (int,))):
    pass


print(type(Girl))  # <class '__main__.MyType'>
print(getattr(Girl, "name"))  # 夏色祭

所以個位應該明白下面的代碼是做什么的了,Python虛擬機就是將基類的metaclass設置為子類的metaclass。對於當前的PyType_Type來說,其metaclass就是object的metaclass,也是它自己,而在源碼的PyBaseObject_Type中可以看到其ob_type是被設置成了PyType_Type的。

	//設置type信息
    if (Py_TYPE(type) == NULL && base != NULL)
        Py_TYPE(type) = Py_TYPE(base);

既然繼承了PyBaseObject_Type,那么便會首先初始化PyBaseObject_Type,我們下面來看看這個PyBaseObject_Type、Python中的object是怎么被初始化的。

處理基類列表

接下來,Python虛擬機會處理類型的基類列表,因為Python支持多重繼承,所以每一個Python的類對象都會有一個基類、或者說父類列表。

int
PyType_Ready(PyTypeObject *type)
{
    PyObject *dict, *bases;
    PyTypeObject *base;
    Py_ssize_t i, n;

    //獲取tp_base中指定的基類
    base = type->tp_base;
    if (base == NULL && type != &PyBaseObject_Type) {
        base = type->tp_base = &PyBaseObject_Type;
        Py_INCREF(base);
    }
	
    ...
    ...
    ...    

    //處理bases:基類列表
    bases = type->tp_bases;
    //如果bases為空
    if (bases == NULL) {
        //如果base也為空,說明這個類型對象一定是PyBaseObject_Type
        //因為Python中任何類都繼承自object,除了object自身
        if (base == NULL)
            //那么這時候bases就是個空元組,元素個數為0
            bases = PyTuple_New(0);
        else
            //否則的話,就申請只有一個空間的元素,然后將PyBaseObject_Type塞進去
            bases = PyTuple_Pack(1, base);
        if (bases == NULL)
            goto error;
        //設置bases
        type->tp_bases = bases;
    }
}

因此我們看到有兩個屬性,一個是tp_base,一個是tp_bases,我們看看這倆在Python中的區別。

class A:
    pass


class B(A):
    pass


class C:
    pass


class D(B, C):
    pass


print(D.__base__)  # <class '__main__.B'>
print(D.__bases__)  # (<class '__main__.B'>, <class '__main__.C'>)

print(C.__base__)  # <class 'object'>
print(C.__bases__)  # (<class 'object'>,)

print(B.__base__)  # <class '__main__.A'>
print(B.__bases__)  # (<class '__main__.A'>,)

我們看到D同時繼承多個類,那么tp_base就是先出現的那個基類,而tp_bases則是繼承的所有基類,但是基類的基類是不會出現的,比如object。對於class B也是一樣的。然后我們看看class C,因為C沒有顯式地繼承任何類,那么tp_bases就是NULL,但是Python3中所有的類都默認繼承了object,所以tp_base就是PyBaseObject_Type,那么就會把tp_base拷貝到tp_bases里面,因此也就出現了這個結果。

填充tp_dict

在設置完類型和基類之后,下面Python虛擬機就進入了激動人心的tp_dict的填充階段,也就是設置屬性字典,這是一個極其繁復、極其繁復、極其繁復的過程。

int
PyType_Ready(PyTypeObject *type)
{
    PyObject *dict, *bases;
    PyTypeObject *base;
    Py_ssize_t i, n;

    //......
    //......
    //......   
    //初始化tp_dict
    dict = type->tp_dict;
    if (dict == NULL) {
        dict = PyDict_New();
        if (dict == NULL)
            goto error;
        type->tp_dict = dict;
    }

    //將與type相關的操作加入到tp_dict中
    //注意: 這里的type是PyType_Ready的參數中的type, 所以它可以是Python中的<class 'type'>、也可以是<class 'int'>
    if (add_operators(type) < 0)
        goto error;
    if (type->tp_methods != NULL) {
        if (add_methods(type, type->tp_methods) < 0)
            goto error;
    }
    if (type->tp_members != NULL) {
        if (add_members(type, type->tp_members) < 0)
            goto error;
    }
    if (type->tp_getset != NULL) {
        if (add_getset(type, type->tp_getset) < 0)
            goto error;
    }

    //.......
}

在這個階段,完成了將("__sub__", &long_sub)加入tp_dict的過程,里面的 add_operatorsadd_methodsadd_membersadd_getset 都是完成填充tp_dict的動作。那么這時候一個問題就出現了,Python是如何知道__add__和long_add之間存在關聯的呢?其實這種關聯顯然是一開始就已經定好了的,而且存放在一個名為 slotdefs 的數組中。

slot與操作排序

在進入填充tp_dict的復雜操作之前,我們先來看一下Python中的一個概念:slot。在Python內部,slot可以視為表示PyTypeObject中定義的操作,一個操作對應一個slot,但是slot又不僅僅包含一個函數指針,它還包含一些其它信息,我們看看它的結構。在Python內部,slot是通過slotdef這個結構體來實現的。

//typeobject.c
typedef struct wrapperbase slotdef;

//descrobject.h
struct wrapperbase {
    const char *name;
    int offset;
    void *function;
    wrapperfunc wrapper;
    const char *doc;
    int flags;
    PyObject *name_strobj;
};  //從定義上看, 我們發現slot不是一個PyObject

在一個slot中,就存儲着PyTypeObject中一種操作對應的各種信息,比如:int實例對象(PyLongObject)支持哪些行為,就看類型對象int(PyLong_Type)定義了哪些操作,而PyTypeObject對象中的一個操作就會有一個slot與之對應。slot里面的name就是操作對應的名稱,比如字符串__sub__,offset則是操作的函數地址在PyHeapTypeObject中的偏移量,而function則指向一種名為slot function的函數。

Python中提供了多個宏來定義一個slot,其中最基本是TPSLOT和ETSLOT。

//typeobject.c
#define TPSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \
    {NAME, offsetof(PyTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \
     PyDoc_STR(DOC)}

#define ETSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \
    {NAME, offsetof(PyHeapTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \
     PyDoc_STR(DOC)}

我們發現 PyHeapTypeObject 的第一個域就是 PyTypeObject ,因此可以發現TPSLOT計算出的也是 PyHeapTypeObject 的偏移量。

對於一個 PyTypeObject 來說,有的操作,比如long_add,其函數指針是在 PyNumberMethods 里面存放的,而 PyTypeObject 中卻是通過一個tp_as_number指針指向另一個 PyNumberMethods 結構,因此這種情況是沒辦法計算出long_add在 PyTypeObject 中的偏移量的,只能計算出在 PyHeapTypeObject 中的偏移量,這種時候TPSLOT就失效了。

因此與long_add對應的slot必須是通過ETSLOT來定義的,但是我們說 PyHeapTypeObject 里面的offset表示的是基於 PyHeapTypeObject 得到的偏移量,而PyLong_Type卻是一個 PyTypeObject 對象,那么通過這個偏移量顯然無法得到PyLong_Type中為int准備的long_add,那~~~這個offset有什么用呢?

答案非常詭異,這個offset是用來對操作進行排序的。排序?我整個人都不好了,不過在理解為什么需要對操作進行排序之前,需要先看看Python底層預先定義的slot集合--slotdefs。

//typeobject.c
#define SQSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \
    ETSLOT(NAME, as_sequence.SLOT, FUNCTION, WRAPPER, DOC)

static slotdef slotdefs[] = {
    
    //不同操作名(__add__、__radd__)對象, 對應相同操作nb_add
    //這個nb_add在PyLong_Type就是long_add,表示 +
    BINSLOT("__add__", nb_add, slot_nb_add,
           "+"),
    RBINSLOT("__radd__", nb_add, slot_nb_add,
           "+"),
    BINSLOT("__sub__", nb_subtract, slot_nb_subtract,
           "-"),
    RBINSLOT("__rsub__", nb_subtract, slot_nb_subtract,
           "-"),
    BINSLOT("__mul__", nb_multiply, slot_nb_multiply,
           "*"),
    RBINSLOT("__rmul__", nb_multiply, slot_nb_multiply,
           "*"),
    
    //相同操作名(__getitem__)對應不同操作(mp_subscript、mp_ass_subscript)
    MPSLOT("__getitem__", mp_subscript, slot_mp_subscript,
           wrap_binaryfunc,
           "__getitem__($self, key, /)\n--\n\nReturn self[key]."),
    SQSLOT("__getitem__", sq_item, slot_sq_item, wrap_sq_item,
           "__getitem__($self, key, /)\n--\n\nReturn self[key]."),
};

其中BINSLOT,SQSLOT等這些宏實際上都是對ETSLOT的一個簡單包裝,並且在slotdefs中可以發現,操作名(比如__add__)和操作並不是一一對應的,存在多個操作對應同一個操作名、或者多個操作名對應同一個操作的情況,那么在填充tp_dict時,就會出現問題,比如對於__getitem__,在tp_dict中與其對應的是mp_subscript還是sq_item呢?

為了解決這個問題,就需要利用slot中的offset信息對slot(也就是操作)進行排序。回顧一下前面列出的 PyHeapTypeObject 的代碼,它與一般的struct定義不同,其中定義中各個域的順序是非常關鍵的,在順序中隱含着操作優先級的問題。比如在 PyHeapTypeObject 中,PyMappingMethods 的位置在 PySequenceMethods 之前,mp_subscript是 PyMappingMethods 中的一個域:PyObject *,而sq_item又是 PySequenceMethods 中的的一個域:PyObject *,那么最終計算出的偏移量就存在如下關系:offset(mp_subscript) < offset(sq_item)。因此如果在一個PyTypeObject中,既定義了mp_subscript,又定義了sq_item,那么Python虛擬機將選擇mp_subscript與__getitem__發生關系。

而對slotdefs的排序在init_slotdefs中完成:

//typeobject.c
static int slotdefs_initialized = 0;


static void
init_slotdefs(void)
{
    slotdef *p;
    //init_slotdefs只會進行一次
    if (slotdefs_initialized)
        return;
    for (p = slotdefs; p->name; p++) {
        /* Slots must be ordered by their offset in the PyHeapTypeObject. */
        //注釋也表名:slots一定要通過它們在PyHeapTypeObject中的offset進行排序
        //而且是從小到大排
        assert(!p[1].name || p->offset <= p[1].offset);
        p->name_strobj = PyUnicode_InternFromString(p->name);
        if (!p->name_strobj || !PyUnicode_CHECK_INTERNED(p->name_strobj))
            Py_FatalError("Out of memory interning slotdef names");
    }
    
    //排完序之后將值賦為1, 這樣的話下次執行的時候, 執行到上面的if時,由於條件為真就直接return了
    slotdefs_initialized = 1;
}

從slot到descriptor

在slot中,包含了很多關於一個操作的信息,但是很可惜,在tp_dict中,與"__getitem__"關聯在一起的,一定不會是slot,因為它不是一個PyObject,無法將其指針放在dict對象中。當然如果再深入思考一下,會發現slot也無法被調用。因為slot不是一個PyObject,那么它就沒有ob_type這個域,也就無從談起什么tp_call了,所以slot是無論如也無法滿足Python中的可調用這一條件的。前面我們說過,Python虛擬機在tp_dict找到__getitem__對應的操作后,會調用該操作,所以tp_dict中與__getitem__對應的只能是包裝了slot的PyObject。在Python中,我們稱之為descriptor。

在Python內部,存在多種descriptor,與descriptor相對應的是 PyWrapperDescrObject 。在后面的描述中也會直接使用descriptor代表 PyWrapperDescrObject 。一個descriptor包含一個slot,其創建是通過 PyDescr_NewWrapper 完成的。

//descrobject.h
#define PyDescr_COMMON PyDescrObject d_common

typedef struct {
    PyObject_HEAD
    PyTypeObject *d_type;
    PyObject *d_name;
    PyObject *d_qualname;
} PyDescrObject;

typedef struct {
    PyDescr_COMMON;
    struct wrapperbase *d_base;
    void *d_wrapped; /* This can be any function pointer */
} PyWrapperDescrObject;
//descrobject.c
static PyDescrObject *
descr_new(PyTypeObject *descrtype, PyTypeObject *type, const char *name)
{
    PyDescrObject *descr;

    descr = (PyDescrObject *)PyType_GenericAlloc(descrtype, 0);
    if (descr != NULL) {
        Py_XINCREF(type);
        descr->d_type = type;
        descr->d_name = PyUnicode_InternFromString(name);
        if (descr->d_name == NULL) {
            Py_DECREF(descr);
            descr = NULL;
        }
        else {
            descr->d_qualname = NULL;
        }
    }
    return descr;
}



PyObject *
PyDescr_NewWrapper(PyTypeObject *type, struct wrapperbase *base, void *wrapped)
{
    PyWrapperDescrObject *descr;

    descr = (PyWrapperDescrObject *)descr_new(&PyWrapperDescr_Type,
                                             type, base->name);
    if (descr != NULL) {
        descr->d_base = base;
        descr->d_wrapped = wrapped;
    }
    return (PyObject *)descr;
}

Python內部的各種descriptor都將包含 PyDescr_COMMON ,其中的d_type被設置為PyDescr_NewWrapper的參數type,而d_wrapped則存放着最重要的信息:操作對應的函數指針,比如對於PyList_Type來說,其tp_dict["__getitem__"].d_wrapped就是&mp_subscript。而slot則被存放在了d_base中。

當然, PyWrapperDescrObject 里面的type是 PyWrapperDescr_Type ,其中的tp_call是 wrapperdescr_call ,當Python虛擬機調用一個descriptor時,也就會調用 wrapperdescr_call 。對於descriptor的調用過程,我們將在后面詳細介紹。

print(int.__sub__)  # <slot wrapper '__sub__' of 'int' objects>
print(str.__add__)  # <slot wrapper '__add__' of 'str' objects>
print(str.__getitem__)  # <slot wrapper '__getitem__' of 'str' objects>

我們看到它們都是一個slot wrapper,也就是對slot包裝之后的descriptor(描述符)。

建立聯系

排序后的結果仍然存放在slotdefs中,python虛擬機這下就可以從頭到尾遍歷slotdefs,基於每一個slot建立一個descriptor,然后在tp_dict中建立從操作名到descriptor的關聯,這個過程是在add_operators中完成的。

static int
add_operators(PyTypeObject *type)
{
    PyObject *dict = type->tp_dict;
    slotdef *p;
    PyObject *descr;
    void **ptr;
    //對slotdefs進行排序
    init_slotdefs();
    for (p = slotdefs; p->name; p++) {
        //如果slot中沒有指定wrapper,則無需處理
        if (p->wrapper == NULL)
            continue;
        //獲得slot對應的操作在PyTypeObject中的函數指針
        ptr = slotptr(type, p->offset);
        if (!ptr || !*ptr)
            continue;
        //如果tp_dict中已經存在操作名,則放棄
        if (PyDict_GetItemWithError(dict, p->name_strobj))
            continue;
        if (PyErr_Occurred()) {
            return -1;
        }
        if (*ptr == (void *)PyObject_HashNotImplemented) {
            /* Classes may prevent the inheritance of the tp_hash
               slot by storing PyObject_HashNotImplemented in it. Make it
               visible as a None value for the __hash__ attribute. */
            if (PyDict_SetItem(dict, p->name_strobj, Py_None) < 0)
                return -1;
        }
        else {
            //創建descriptor
            descr = PyDescr_NewWrapper(type, p, *ptr);
            if (descr == NULL)
                return -1;
            //將(操作名,descriptor)放入tp_dict中
            if (PyDict_SetItem(dict, p->name_strobj, descr) < 0) {
                Py_DECREF(descr);
                return -1;
            }
            Py_DECREF(descr);
        }
    }
    if (type->tp_new != NULL) {
        if (add_tp_new_wrapper(type) < 0)
            return -1;
    }
    return 0;
}

在add_operators中,首先調用前面剖析過的init_slotdefs對操作進行排序,然后遍歷排序完成后的slotdefs結構體數組,對其中的每一個slot(slotdef),通過slotptr獲得該slot對應的操作在PyTypeObject中的函數指針,,並接着創建descriptor,在tp_dict中建立從操作名(slotdef.name_strobj)到操作(descriptor)的關聯。

但是需要注意的是,在創建descriptor之前,Python虛擬機會檢查在tp_dict中操作名是否存在,如果存在了,則不會再次建立從操作名到操作的關聯。不過也正是這種檢查機制與排序機制相結合,Python虛擬機在能在擁有相同操作名的多個操作中選擇優先級最高的操作。

在add_operators中,上面的動作都很簡單、直觀,而最難的動作隱藏在slotptr這個函數當中。它的功能是完成從slot到slot對應操作的真實函數指針的轉換。我們知道,在slot中存放着用來操作的offset,但不幸的是,這個offset是相對於 PyHeapTypeObject 的偏移,而操作的真實函數指針卻是在 PyTypeObject 中指定的,而且 PyTypeObjectPyHeapTypeObject 不是同構的,因為 PyHeapTypeObject 中包含了 PyNumberMethods 結構體,但 PyTypeObject 只包含了 PyNumberMethods * 指針。所以slot中存儲的關於操作的offset對 PyTypeObject 來說,不能直接用,必須通過轉換。

舉個栗子,假如說調用slotptr(&PyList_Type, offset(PyHeapTypeObject, mp_subscript)),首先判斷這個偏移量大於offset(PyHeapTypeObject, as_mapping),所以會先從PyTypeObject對象中獲得as_mapping指針p,然后在p的基礎上進行偏移就可以得到實際的函數地址。

所以偏移量delta為:offset(PyHeapTypeObject, mp_subscript) - offset(PyHeapTypeObject, as_mapping)

而這個復雜的過程就在slotptr中完成:

static void **
slotptr(PyTypeObject *type, int ioffset)
{
    char *ptr;
    long offset = ioffset;

    /* Note: this depends on the order of the members of PyHeapTypeObject! */
    assert(offset >= 0);
    assert((size_t)offset < offsetof(PyHeapTypeObject, as_buffer));
    //從PyHeapTypeObject中排在后面的PySequenceMethods開始判斷,然后向前,依次判斷PyMappingMethods和PyNumberMethods呢。
    /*
    為什么要這么做呢?假設我們首先從PyNumberMethods開始判斷
    如果一個操作的offset大於在PyHeapTypeObject中的as_numbers在PyNumberMethods的偏移量,那么我們還是沒辦法確認這個操作到底是屬於誰的。只有從后往前進行判斷,才能解決這個問題。
    */    
    if ((size_t)offset >= offsetof(PyHeapTypeObject, as_sequence)) {
        ptr = (char *)type->tp_as_sequence;
        offset -= offsetof(PyHeapTypeObject, as_sequence);
    }
    else if ((size_t)offset >= offsetof(PyHeapTypeObject, as_mapping)) {
        ptr = (char *)type->tp_as_mapping;
        offset -= offsetof(PyHeapTypeObject, as_mapping);
    }
    else if ((size_t)offset >= offsetof(PyHeapTypeObject, as_number)) {
        ptr = (char *)type->tp_as_number;
        offset -= offsetof(PyHeapTypeObject, as_number);
    }
    else if ((size_t)offset >= offsetof(PyHeapTypeObject, as_async)) {
        ptr = (char *)type->tp_as_async;
        offset -= offsetof(PyHeapTypeObject, as_async);
    }
    else {
        ptr = (char *)type;
    }
    if (ptr != NULL)
        ptr += offset;
    return (void **)ptr;
}

好了,我想到現在我們應該能夠摸清楚Python在改造PyTypeObject對象時對tp_dict做了什么了,我們以PyList_Type舉例說明:

在add_operators完成之后,PyList_Type如圖所示。從PyList_Type.tp_as_mapping中延伸出去的部分是在編譯時就已經確定好了的,而從tp_dict中延伸出去的部分則是在Python運行時環境初始化的時候才建立的。

另外, PyType_Ready 在通過add_operators添加了 PyTypeObject 對象中定義的一些operator后,還會通過add_methods、add_numbers和add_getsets添加 PyTypeObject 中定義的tp_methods、tp_members和tp_getset函數集。這些過程和add_operators類似,不過最后添加到tp_dict中descriptor就不再是 PyWrapperDescrObject ,而分別是 PyMethodDescrObjectPyMemberDescrObjectPyGetSetDescrObject

print(str.__add__)  # <slot wrapper '__add__' of 'str' objects>
print(list.__add__)  # <slot wrapper '__add__' of 'list' objects>

print(str.__getitem__)  # <slot wrapper '__getitem__' of 'str' objects>
print(list.__getitem__)  # <method '__getitem__' of 'list' objects>

# 我們看到對於list的__getitem__來說, 就不再是PyWrapperDescrObject(slot wrapper)了
# 而是一個PyMethodDescrObject

從目前來看,基本上算是解析完了,但是還有一點:

class A(list):

    def __repr__(self):
        return "xxx"


a = A()
print(a)  # xxx

顯然當我們print(a)的時候,應該調用A.tp_repr函數,對照PyList_Type的布局,應該調用list_repr這個函數,然而事實卻並非如此,Python虛擬機調用的是我們在A中重寫的__repr__方法。這意味着Python在初始化A的時候,對tp_repr進行了特殊處理。為什么Python虛擬機會知道要對tp_repr進行特殊處理呢?當然肯定有人會說:這是因為我們重寫了__repr__方法啊,確實如此,但這是Python層面上的,在底層的話,答案還是在slot身上。

//typeobject.c
static slotdef slotdefs[] = {
    ...
    TPSLOT("__repr__", tp_repr, slot_tp_repr, wrap_unaryfunc,
           "__repr__($self, /)\n--\n\nReturn repr(self)."),
    ...
} 

Python虛擬機在初始化A時,會檢查A的tp_dict中是否存在__repr__,在后面剖析自定義class對象的創建時會看到,因為在定義class A的時候,重寫了__repr__這個操作。所以在A.tp_dict中,__repr__一開始就會存在,Python虛擬機會檢測到,然后會根據__repr__對應的slot順藤摸瓜,找到tp_repr,並且將這個函數指針替換為slot中指定的&slot_tp_repr。所以當后來虛擬機找A.tp_repr的時候,實際上找的是slot_tp_repr。

//typeobject.c
static PyObject *
slot_tp_repr(PyObject *self)
{
    PyObject *func, *res;
    _Py_IDENTIFIER(__repr__);
    int unbound;
    //查找__repr__屬性
    func = lookup_maybe_method(self, &PyId___repr__, &unbound);
    if (func != NULL) {
        //調用__repr__對應的對象
        res = call_unbound_noarg(unbound, func, self);
        Py_DECREF(func);
        return res;
    }
    PyErr_Clear();
    return PyUnicode_FromFormat("<%s object at %p>",
                               Py_TYPE(self)->tp_name, self);
}

在slot_tp_repr中,會尋找__repr__屬性對應的對象,正好就會找到在A中重寫的函數,后面會看到,這個對象實際上就一個PyFunctionObject對象。這樣一來,就完成了對默認的list的repr行為的替換。所以對於A來說,內存布局就是下面這樣。

當然這僅僅是針對於__repr__,對於其他的操作還是會指向PyList_Type中指定的函數,比如tp_iter還是會指向list_iter。因為我們的類A繼承list,所以如果某個函數在A里面沒有的話,那么會 從PyList_Type中尋找。

對於A來說,這個變化是在 fixup_slot_dispatchers 這個函數中完成的,對於內建對象則不會進行此操作,因為內建對象是被靜態初始化的,它不允許屬性的動態設置。

//typeobject.c
static void
fixup_slot_dispatchers(PyTypeObject *type)
{
    slotdef *p;

    init_slotdefs();
    for (p = slotdefs; p->name; )
        p = update_one_slot(type, p);
}

確定MRO

MRO,即method resolve order,說白了就是類繼承之后、屬性或方法的查找順序。如果Python是單繼承的話,那么這就不是問題了,直接一層一層網上找就可以了。但是Python是支持多繼承的,那么在多繼承時,繼承的順序就成為了一個必須考慮的問題。

class A:

    def foo(self):
        print("A")


class B(A):
    def foo(self):
        print("B")


class C(A):

    def foo(self):
        print("C")
        self.bar()

    def bar(self):
        print("bar C")


class D(C, B):
    def bar(self):
        print("bar D")


d = D()
d.foo()
"""
C
bar D
"""

首先我們看到,打印的是C,說明調用的是C的foo函數,這說明把C寫在前面,會調用C的方法。但是下面打印了bar D,這是因為C里面的self,實際上是D的實例對象。D在找不到foo函數的時候,會到父類里面找,但是同時也會將self傳遞過去,所以調用self.bar的時候,還是會先到D里面找,如果找不到再去父類里面找。

在底層則是先在PyType_Ready中通過mro_internal確定mro的順序,Python虛擬機將創建一個PyTupleObject對象,里面存放一組類對象,這些類對象的順序就是虛擬機確定的mro的順序,最終這個PyTuple對象會被保存在PyTypeObject.tp_mro中。

由於mro_internal內部的實現機制相當復雜,所以我們將會只從python的代碼層面來理解。首先我們說python早期有經典類和新式類兩種類,現在則只存在新式類。而經典類的類搜索方式采用的是深度優先,而新式類則是廣度優先(當然現在用的是新的算法,具體什么算法后面說,暫時理解為廣度優先即可),舉個例子:

圖中的箭頭表示繼承關系,比如:A繼承B和C、B繼承D、C繼承E。

對於上圖來說,經典類和新式類的查找方式是一樣的:先從A找到I,再從C找到G。對於上圖這種繼承結構,經典類和新式類是一樣的,至於兩邊是否一樣多則不重要。我們實際演示一下,由於經典類只在Python2中存在,所以下面我們演示新式類。

# 這里是python3.8 新式類
I = type("I", (), {})
H = type("H", (I,), {})
F = type("F", (H,), {})
G = type("G", (), {})
D = type("D", (F,), {})
E = type("E", (G,), {})
B = type("B", (D,), {})
C = type("C", (E,), {})
A = type("A", (B, C), {})

for _ in A.__mro__:
    print(_)
"""
<class '__main__.A'>
<class '__main__.B'>
<class '__main__.D'>
<class '__main__.F'>
<class '__main__.H'>
<class '__main__.I'>
<class '__main__.C'>
<class '__main__.E'>
<class '__main__.G'>
<class 'object'>
"""

對於A繼承兩個類,這個兩個類分別繼續繼承,如果最終沒有繼承公共的類(暫時先忽略object),那么經典類和新式類是一樣的。像這種涇渭分明、各自繼承各自的,都是先一條路找到黑,然后再去另外一條路去找。

但如果是下面這種,最終分久必合、兩者最終又繼承了同一個類,那么經典類還是跟以前一樣,按照每一條路都走到黑的方式。但是對於新式類,則是先從A找到H,而I這個兩邊最終繼承的類不找了,然后從C找到I,也就是在另一條路找到頭。

# 新式類
I = type("I", (), {})
H = type("H", (I,), {})
F = type("F", (H,), {})
G = type("G", (I,), {})   # 這里讓G繼承I
D = type("D", (F,), {})
E = type("E", (G,), {})
B = type("B", (D,), {})
C = type("C", (E,), {})
A = type("A", (B, C), {})

for _ in A.__mro__:
    print(_)
"""
<class '__main__.A'>
<class '__main__.B'>
<class '__main__.D'>
<class '__main__.F'>
<class '__main__.H'>
<class '__main__.C'>
<class '__main__.E'>
<class '__main__.G'>
<class '__main__.I'>
<class 'object'>
"""

因此對於最下面的類繼承兩個類,然后繼承的兩個類再次繼承的時候,向上只繼承一個類,對於這種模式,那么結論、也就是mro順序就是我們上面分析的那樣。不過對新式類來說,因為所有類默認都是繼承object,所以第一張圖中,即使我們沒畫完,但是也能想到,兩條涇渭分明的繼承鏈的上方最終應該都指向object。那么我們依舊可以用剛才的理論來解釋,在第一條繼承鏈中找到object的前一個類不找了,然后在第二條繼承鏈中一直找到object。

但是Python的多繼承遠比我們想象的要復雜,原因就在於可以任意繼承,如果B和C再分別繼承兩個類呢?那么我們這里的線路就又要多出兩條了,不過既然要追求刺激,就貫徹到底嘍。但是下面我們就只會介紹新式類了,經典類了解一下就可以了。

另外我們之前說新式類采用的是廣度優先,但是實際上這樣有一個問題:

假設我們調用A的foo方法,但是A里面沒有,那么理所應當會去B里面找,但是B里面也沒有,而C和D里面有,那么這個時候是去C里面找還是去D里面找呢?根據我們之前的結論,顯然是去D里面找,可如果按照廣度優先的邏輯來說,那么應該是去C里面找啊。所以廣度優先理論在這里就不適用了,因為B繼承了D,而B和C並沒有直接關系,我們應該把B和D看成一個整體。因此Python中的廣度優先實際上是采用了一種叫做C3的算法。

這個C3算法比較復雜(其實也不算復雜),只不過我個人總結出一個更加好記的結論,如下:

當沿着一條繼承鏈尋找類時,默認會該繼承鏈一直找下去,但如果發現某個類出現在了另一條繼承鏈當中,那么當前的繼承鏈的搜索就會結束,然后在"最開始"出現分歧的地方轉向下一條繼承鏈的搜索。

這是我個人總結的,或許光看字面意思的話會比較難理解,但是通過例子就能明白了。

這個箭頭表示繼承關系,繼承順序是從左到右,比如這里的A就相當於class A(B, C),下面我們來從頭到尾分析一下。

  • 1. 首先最開始的順序是A, 如果我們獲取A的mro的話;
  • 2. 然后A繼承B和C, 由於是兩條路, 因此我們說A這里就是一個分歧點。但是由於B在前, 所以接下來是B, 所以現在mro的順序是A B;
  • 3. 但是B這里也出現了分歧點, 不過不用管, 因為我們說會沿着繼承鏈不斷往下搜索, 現在mro的順序是A B D;
  • 4. 然后從D開始尋找, 這里注意了, 按理說會找G的, 但是G不止被一個類繼承, 也就是意味着沿着當前的繼承鏈查找G時, G還出現在了其它的繼承鏈當中。怎么辦?顯然要回到最初的分歧點, 轉向下一條繼承鏈的搜索;
  • 5. 最初的分歧點是A, 那么該去找C了, 現在mro的順序就是A B D C;
  • 6. 注意C這里出現了分歧點, 而A的兩條分支已經結束了, 所以現在C就是最初的分歧點了。而C繼承自E和F, 顯然要搜索E, 那么此時mro的順序就是A B D C E;
  • 7. 然后從E開始搜索, 顯然要搜索G, 此時mro順序是A B D C E G;
  • 8. 從G要搜索I, 此時mro順序是A B D C E G I;
  • 9. 從I開始搜索誰呢?由於J出現在了其它的繼承鏈中, 那么要回到最初分歧的地方, 也就是C, 那么下面顯然要找F, 此時mro順序是A B D C E G I F;
  • 10. F只繼承了H, 那么肯定要找H, 此時mro順序是 A B D C E G I F H;
  • 11. H顯然只能找J了, 因此最終A的mro順序就是A B D C E G I F H J object;
J = type("J", (object, ), {})
I = type("I", (J, ), {})
H = type("H", (J, ), {})
G = type("G", (I, ), {})
F = type("F", (H, ), {})
E = type("E", (G, H), {})
D = type("D", (G, ), {})
C = type("C", (E, F), {})
B = type("B", (D, E), {})
A = type("A", (B, C), {})

# A B D C E G I F H J
for _ in A.__mro__:
    print(_)
"""
<class '__main__.A'>
<class '__main__.B'>
<class '__main__.D'>
<class '__main__.C'>
<class '__main__.E'>
<class '__main__.G'>
<class '__main__.I'>
<class '__main__.F'>
<class '__main__.H'>
<class '__main__.J'>
<class 'object'>
"""

我們再看一個復雜的例子感受一下:

看起來很花里胡哨的,但其實很簡單,就按照之前說的那個結論不斷推導下去即可。

  • 1. 首先是A, A繼承B1、B2、B3, 會先走B1, 此時mro是A B1, 注意現在A是分歧點;
  • 2. 從B1本來該找C1, 但是C1還被其他類繼承, 也就是出現在了其它的繼承鏈當中, 因此要回到最初分歧點A, 從下一條繼承鏈開始找, 顯然要找B2, 此時mro就是A B1 B2;
  • 3. 從B2開始, 顯然要找C1, 此時mro順序就是A B1 B2 C1;
  • 4. 從C1開始, 顯然要找D1, 因為D1只被C1繼承, 所以它沒有出現在另一條繼承鏈當中, 因此此時mro順序是A B1 B2 C1 D1;
  • 5. 從D1顯然不會找E的, 咋辦? 回到最初的分歧點, 注意這里顯然還是A, 因為A的分支還沒有走完。顯然此時要走B3, 那么mro順序就是A B1 B2 C1 D1 B3;
  • 6. 從B3開始找, 顯然要找C2, 注意: A的分支已經走完, 此時B3就成了新的最初分歧點。現在mro順序是A B1 B2 C1 D1 B3 C2;
  • 7. C2會找D2嗎? 顯然不會, 因為它還被C3繼承, 所以它出現在了其他的繼承鏈中。所以要回到最初分歧點, 這里是B3, 顯然下面要找C3, 另外由於B3的分支也已經走完, 所以現在C3就成了新的最初分歧點。此時mro順序是A B1 B2 C1 D1 B3 C2 C3;
  • 8. 從C3開始, 顯然要找D2, 此時mro順序是A B1 B2 C1 D1 B3 C2 C3 D2;
  • 9. 但是D2不會找E, 因此回到最初分歧點C3, 下面就找D3, 然后顯然只能再找E了, 顯然最終mro順序A B1 B2 C1 D1 B3 C2 C3 D2 D3 E object;
E = type("E", (), {})
D1 = type("D1", (E,), {})
D2 = type("D2", (E,), {})
D3 = type("D3", (E,), {})
C1 = type("C1", (D1, D2), {})
C2 = type("C2", (D2,), {})
C3 = type("C3", (D2, D3), {})
B1 = type("B1", (C1,), {})
B2 = type("B2", (C1, C2), {})
B3 = type("B3", (C2, C3), {})
A = type("A", (B1, B2, B3), {})

for _ in A.__mro__:
    print(_)
"""
<class '__main__.A'>
<class '__main__.B1'>
<class '__main__.B2'>
<class '__main__.C1'>
<class '__main__.D1'>
<class '__main__.B3'>
<class '__main__.C2'>
<class '__main__.C3'>
<class '__main__.D2'>
<class '__main__.D3'>
<class '__main__.E'>
<class 'object'>
"""

因此Python的多繼承並沒有我們想象的那么復雜,當然底層源碼我們就不再看了,這個東西分析起來沒什么太大必要,有興趣可以自己去看一下。個人覺得,關於多繼承從目前這個層面上來理解已經足夠了。

不過需要注意的是,在執行父類函數時傳入的self參數,這一點是很多初學者容易犯的錯誤。

class A:

    def foo(self):
        print("A: foo")
        self.bar()

    def bar(self):
        print("A: bar")


class B:

    def bar(self):
        print("B: bar")


class C(A, B):

    def bar(self):
        print("C: bar")


C().foo()
"""
A: foo
C: bar
"""

首先C的實例對象在調用foo的時候,首先會去C里面查找,但是C沒有,所以按照mro順序會去A里面找。而A里面存在,所以調用,但是:調用時傳遞的self是C的實例對象,因為是C的實例對象調用的。所以里面的self.bar,這個self還是C的實例對象,那么調用bar的時候,會去哪里找呢?顯然還是從C里面找,所以 self.bar() 的時候打印的是"C: bar",而不是"A: bar"。

同理再來看看一個關於super的栗子:

class A:

    def foo(self):
        super(A, self).foo()

class B:

    def foo(self):
        print("B: foo")


class C(A, B):
    pass


try:
    A().foo()
except Exception as e:
    print(e)  # 'super' object has no attribute 'foo'

# 首先A的父類是object, 所以super(A, self).foo()的時候回去執行object的foo
# 但是object沒有foo, 所以報錯了, 報錯信息中的'super'指的就是A的父類
# 但是, 是的, 我要說但是了
C().foo()  # B: foo

"""
如果是C()調用foo的話, 最終卻執行了B的foo函數, 這是什么原因呢?
首先C里面里面沒有foo, 那么會去執行A的foo, 但是執行時候的self是C的實例對象, super里面的self也是C里面的self
然后我們知道對於C而言, 其mro是 C、A、B、object

所以super(A, self).foo() 就表示: 沿着繼承鏈 C、A、B、object的順序去找foo函數
但是super里面有一個A, 表示不要從頭開始找, 而是從A的后面開始找, 所以下一個就找到B了
"""

所以說super不一定就是父類,而是要看里面的self是誰。總之:super(xxx, self)一定是type(self)對應的mro中,xxx的下一個類。

繼承基類操作

python虛擬機確定了mro順序列表之后,就會遍歷mro列表(第一個類對象會是其自身,比如A.__mro__的第一個元素就是A本身,所以遍歷是從第二項開始的)。在mro列表中實際上存儲的就是類對象的所有直接基類、間接基類,Python虛擬機會將自身沒有、但是基類(注意:包括間接基類,比如基類的基類)中存在的操作拷貝到該類當中,從而完成對基類操作的繼承動作。

而這個繼承操作的動作是發生在inherit_slots中

//typeobject.c
int
PyType_Ready(PyTypeObject *type)
{
    //...
    //...    
    bases = type->tp_mro;
    assert(bases != NULL);
    assert(PyTuple_Check(bases));
    n = PyTuple_GET_SIZE(bases);
    for (i = 1; i < n; i++) {
        PyObject *b = PyTuple_GET_ITEM(bases, i);
        if (PyType_Check(b))
            inherit_slots(type, (PyTypeObject *)b);
    }
    ...
    ...   
}

在inherit_slots中會拷貝相當多的操作,這里就拿nb_add(整型則對應long_add)來舉個栗子:

static void
inherit_slots(PyTypeObject *type, PyTypeObject *base)
{
    PyTypeObject *basebase;

#undef SLOTDEFINED
#undef COPYSLOT
#undef COPYNUM
#undef COPYSEQ
#undef COPYMAP
#undef COPYBUF

#define SLOTDEFINED(SLOT) \
    (base->SLOT != 0 && \
     (basebase == NULL || base->SLOT != basebase->SLOT))

#define COPYSLOT(SLOT) \
    if (!type->SLOT && SLOTDEFINED(SLOT)) type->SLOT = base->SLOT

#define COPYASYNC(SLOT) COPYSLOT(tp_as_async->SLOT)
#define COPYNUM(SLOT) COPYSLOT(tp_as_number->SLOT)
#define COPYSEQ(SLOT) COPYSLOT(tp_as_sequence->SLOT)
#define COPYMAP(SLOT) COPYSLOT(tp_as_mapping->SLOT)
#define COPYBUF(SLOT) COPYSLOT(tp_as_buffer->SLOT)

    /* This won't inherit indirect slots (from tp_as_number etc.)
       if type doesn't provide the space. */

    if (type->tp_as_number != NULL && base->tp_as_number != NULL) {
        basebase = base->tp_base;
        if (basebase->tp_as_number == NULL)
            basebase = NULL;
        COPYNUM(nb_add);
        COPYNUM(nb_subtract);
        COPYNUM(nb_multiply);
        COPYNUM(nb_remainder);
        COPYNUM(nb_divmod);
        COPYNUM(nb_power);
        COPYNUM(nb_negative);
        COPYNUM(nb_positive);
        COPYNUM(nb_absolute);
        COPYNUM(nb_bool);
        COPYNUM(nb_invert);
        COPYNUM(nb_lshift);
        COPYNUM(nb_rshift);
        COPYNUM(nb_and);
        COPYNUM(nb_xor);
        COPYNUM(nb_or);
        COPYNUM(nb_int);
        COPYNUM(nb_float);
        COPYNUM(nb_inplace_add);
        COPYNUM(nb_inplace_subtract);
        COPYNUM(nb_inplace_multiply);
        COPYNUM(nb_inplace_remainder);
        COPYNUM(nb_inplace_power);
        COPYNUM(nb_inplace_lshift);
        COPYNUM(nb_inplace_rshift);
        COPYNUM(nb_inplace_and);
        COPYNUM(nb_inplace_xor);
        COPYNUM(nb_inplace_or);
        COPYNUM(nb_true_divide);
        COPYNUM(nb_floor_divide);
        COPYNUM(nb_inplace_true_divide);
        COPYNUM(nb_inplace_floor_divide);
        COPYNUM(nb_index);
        COPYNUM(nb_matrix_multiply);
        COPYNUM(nb_inplace_matrix_multiply);
    }
    //......
    //......

我們在里面看到很多熟悉的東西,如果你常用魔法方法的話。而且我們知道PyBool_Type中並沒有設置nb_add,但是PyLong_Type中卻設置了nb_add操作,而bool繼承int。所以對布爾類型是可以直接進行運算的,當然和整型、浮點型運算也是可以的。所以在numpy中,判斷一個數組中多少個滿足條件的元素,可以使用numpy提供的機制進行比較,會得到一個同樣長度的數組,里面的每一個元素為是否滿足條件所對應的布爾值。然后直接通過sum運算即可,因為運算的時候,True會被解釋成1,False會被解釋成0。

import numpy as np

arr = np.array([2, 4, 7, 3, 5])
print(arr > 4)  # [False False  True False  True]
print(sum(arr > 4))  # 2


print(2.2 + True)  # 3.2

所以在python中,整型是可以和布爾類型進行運算的,看似不可思議,但又在情理之中。

填充基類中的子類列表

到這里,PyType_Ready還剩下最后一個重要的動作了:設置基類中的子類列表。在每一個PyTypeObject中,有一個tp_subclasses,這個東西在PyType_Ready完成之后,將會是一個list對象。其中存放着所有直接繼承自類的類對象,PyType_Ready是通過調用add_subclass完成向這個tp_subclasses中填充子類的動作。

int
PyType_Ready(PyTypeObject *type)
{
    PyObject *dict, *bases;
    PyTypeObject *base;
    Py_ssize_t i, n;
	
    //填充基類的子類列表
    bases = type->tp_bases;
    n = PyTuple_GET_SIZE(bases);
    for (i = 0; i < n; i++) {
        PyObject *b = PyTuple_GET_ITEM(bases, i);
        if (PyType_Check(b) &&
            add_subclass((PyTypeObject *)b, type) < 0)
            goto error;
    }
}
print(object.__subclasses__())
# [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, 
# <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, 
# <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, 
# <class 'traceback'>, <class 'super'>, ... ... ...

果然,python里面的object不愧是萬物之父,這么多的內建對象都是繼承自object的。到了這里,我們才算是完整的剖析了PyType_Ready的動作,可以看到,python虛擬機對python的內建對象對應的PyTypeObject進行了多種繁雜的改造工作,可以包括以下幾部分:

  • 設置type信息,基類及基類列表;
  • 填充tp_dict;
  • 確定mro列表;
  • 基於mro列表從基類繼承操作;
  • 設置子類列表;

不同的類型,有些操作也會有一些不同的行為,但整體是一致的。因此具體某個特定類型,可以自己跟蹤PyType_Ready的操作。

小結

我們看到類的屬性查找雖然看起來簡單,但是底層實現起來還是很復雜的。當然關於自定義的類是如何構建的,我們將在下一篇博客中進行剖析。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM