《python解釋器源碼剖析》第13章--python虛擬機中的類機制


13.0 序

這一章我們就來看看python中類是怎么實現的,我們知道C不是一個面向對象語言,而python卻是一個面向對象的語言,那么在python的底層,是如何使用C來支持python實現面向對象的功能呢?帶着這些疑問,我們下面開始剖析python中類的實現機制。另外,在python2中存在着經典類(classic class)和新式類(new style class),但是到Python3中,經典類已經消失了。並且python2官網都快不維護了,因此我們這一章只會介紹新式類。

13.1 python中的對象模型

我們在第一章python對象初探的時候就說了,在面向對象的理論中,有兩個核心的概念:類和實例。類可以看成是一個模板,那么實例就是根據這個模板創建出來的對象。可以想象成docker的鏡像和容器。但是在python中,一切都是對象,所以類和實例都是對象,類叫做類對象,實例叫做實例對象。如果想用大白話來描述清楚的話,這無疑是一場災難,我們還是需要使用一些專業術語來描述:

首先我們這里把python中的對象分為三種

  • 內建對象:python中的內建對象,比如int、str、list、type、object等等
  • class對象:程序員通過python中的class關鍵字定義的類。當然后面我們也會把內建對象和class對象統稱為類對象的
  • 實例對象:表示由內建對象或者class對象創建的實例

13.1.1 對象間的關系

python的三種對象之間,存在着兩種關系

  • is-kind-of:對應面向對象理論中父類和子類之間的關系
  • is-instance-of:對應面向對象理論中類和實例之間的關系
class A(object):
    pass


a = A()

這段代碼中便包含了上面的三種對象:object(內建對象)A(class對象)a(實例對象)。顯然object和A之間是is-kind-of關系,即object是A的父類,另外值得一提的是,在python3中所有定義的類都是默認繼承自object,即便我們這里不顯式寫繼承object,也會默認繼承的,為了說明,我們就寫上了。除了object是A的父類,我們還能看出a和A存在is-instance-of關系,即a是A的實例。當然如果再進一步的話,a和object之間也存在is-instance-of關系,a也是object的實例。我們可以使用python查看一下

class A(object):
    pass


a = A()
print(type(a))  # <class '__main__.A'>
print(isinstance(a, object))  # True

我們看到盡管打印a的類型顯示的是A(內建對象、class對象除了表示對象之外,還可以用來表示對應實例的類型,比如這里a的類型就是A),但是a也是object的實例,因為A繼承了object,至於這其中的原理,我們會慢慢介紹到。

python中的類型檢測

python提供了一些方法可以探測這些關系,除了我們上面的type之外,還可以使用對象的__class__屬性探測一個對象和其它的哪些對象之間存在is-instance-of關系,而通過對象的__bases__屬性則可以探測一個對象和其它的哪些對象之間存在着is-kind-of關系。此外python還提供了兩個方法issubclassisinstance來驗證兩個對象之間是否存在着我們期望的關系

class A(object):
    pass


a = A()
####################
print(a.__class__)  # <class '__main__.A'>
print(A.__class__)  # <class 'type'>

# 因為python可以多繼承,因為打印的是一個元組
print(A.__bases__)  # (<class 'object'>,)
# 另外__class__是查看自己的類型是什么,也就是生成自己的類。
# 而在介紹python對象的時候,我們就看到了,任何一個對象都至少具備兩個東西,一個是引用計數、一個是對象的類型
# 所以__class__在python中,是所有的對象都具備的

# 但是__bases__的話就不一定了,這個屬性實例對象是沒有的。只有class對象、內建對象才有
# 因此是無法調用a.__bases__的

估計看到這張圖就應該知道我想說什么了,里面有着一個非常關鍵、也是非常讓人費解的一個點。我記得之前說過,但是在這里我們再來研究一遍。

13.1.2 <class 'type'>和<class 'object'>

首先記住python中關於類的兩句話:

  • 所有的類對象(內建對象+class對象)都是由type生成的
  • 所有的類對象都繼承object

邏輯性比較強的人,可能馬上就發現了,這兩句話組合起來是存在矛盾的,但是在python中是不矛盾的。我們來看幾個例子

class A:
    pass


print(type(A))  # <class 'type'>
print(type(int))  # <class 'type'>
print(type(dict))  # <class 'type'>

print(A.__bases__)  # (<class 'object'>,)
print(int.__bases__)  # (<class 'object'>,)
print(dict.__bases__)  # (<class 'object'>,)


# 相信上面的都沒有什么問題,但是令人費解的是下面
print(type(object))  # <class 'type'>
print(type.__bases__)  # (<class 'object'>,).

我們看到object這個對象是由type創建的,但是object又是type的父類,那么這就先入了先有雞還是先有蛋的問題。其實這兩者是同時出現的,只不過在python中把兩者形成了一個閉環,也正因為如此,python才能把一切皆對象的理念貫徹的如此徹底。至於type是由誰創建的,很容易猜到是由type自身創建的,連自己都不放過,更不要說其他的類對象了,因次我們也把type稱之為metaclass(元類),創建類對象的類。更具體的解釋請在前面的章節中翻一翻

我們剛才把python中的對象分成了三類:內建對象、class對象、實例對象。但是class對象也可以稱之為實例對象,因為它是type生成的,那么自然就是type的一個實例對象,但是它同時也能生成實例,因此又叫做class對象。但是一般我們就認為實例對象就只是除了type之外的類生成的實例對象

因此現在我們可以總結一下:

  • 在python中,任何一個對象都有一個類型,可以通過對象的__class__屬性獲取,也可以通過type函數去查看。任何一個實例對象的類型都是一個類對象,而類對象的類型則是<class 'type'>。而在python底層,它實際上對應的就是PyType_Type
  • 在python中,任何一個類對象都與<class 'object'>之間存在is-kind-of關系,包括<class 'type'>。在python內部,<class 'object'>對應的類型是PyBaseObject_Type

13.2 深入♂class

我們知道python里面有很多以雙下划線開頭、雙下划線結尾的方法,我們稱之為魔法方法。python中的每一個對象所能進行操作,那么在生成該對象的對象中一定會定義相應的魔法方法。比如整型3,整型可以相加,這就代表int這個類里面肯定定義了__add__方法

class MyInt(int):

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


a = MyInt(1)
b = MyInt(2)
print(a + b)  # 9

我們自己實現了一個類,繼承自int,那么肯定可以接收一個整型。當我執行a+b的時候,肯定執行對應的__add__方法,然后調用int的__add__方法,得到結果之后再乘上3,邏輯上沒有問題。但是問題來了,首先調用int.__add__的時候,python是怎么相加的呢?而且我們知道int.__add__(self, other)里面的參數顯然都應該是int,但是我們傳遞的是MyInt,那么python虛擬機是怎么尋找的呢?先來看一張草圖:

當python虛擬機需要調用int.__add__時,會從對應類型(PyObject里面ob_type域,這是一個PyTypeObject對象)的tp_dict域里面查找符號為__add__的對應的值,通過這個值找到long_as_number中對應的操作,從而完成對int.__add__的調用。

注意:我們上面說通過這個值找到long_as_number中對應的操作,並沒有說指向它,對,很明顯了,tp_dict中__add__對應的值不是直接指向long_add的,而是它的調用(也就是加上括號)才指向它。雖然是調用,但是並不代表這個值就一定是一個PyFunctionObject,在python中一切都有可能被調用,只要對應的類型對象(比如A加上括括號生成了a,那么就把A稱之為a的類型對象,這樣秒速會方便一些)定義了__call__方法,即在底層中的ob_type域中定義的tp_call方法不為NULL。

一言以蔽之:在python中,所謂調用,就是執行類型對象對應的ob_type域中定義的tp_call方法

class A:

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

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

在python內部,實際上是通過一個PyObject_Call的函數對實例對象a進行操作。

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


foo()

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

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

# 其實所有的類都由type生成的,這個__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和類的屬性查找一樣,如果找不到會自動到對應的類型對象里面去找
# type生成了int,那么如果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


print(C()())
"""
<__main__.C object at 0x000002282F3D9B80>
我被尻了
"""
# 此時我們看到,給C的實例對象加括號的時候,C里面沒有__call__方法,這個時候是不會到元類里面找的
# 還是之前的結論,實例對象找不到屬性,會去類對象里面找,然而即便此時類對象里面也沒有,也不會到元類type里面找,這時候就看父類了
# 只有當類對象去找屬性找不到的時候,才會去元類里面找,正如上面的__call__方法
# int里面沒有,但它的類型對象type里面有,所以會去type里面找。如果type再找不到,肯定報錯了
# 我們看對於我們上面的例子,給C的實例對象加括號的時候,會執行C這個類里面的__call__
# 但是它沒有,所以找不到。然而它繼承的父類里面有__call__
# 因此會執行繼承的父類的__call__方法,並且里面的self還是C的實例對象

看,一個整數對象是不可調用的,但是我們發現這並不是在編譯的時候就能夠檢測出來的錯誤,而是在運行時才能檢測出來、會在運行時通過函數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對象的作用還不同。比如說:list和class A:,list就已經作為PyList_Type(PyTypeObject)在python中存在了,咦,PyList_Type是個啥,和PyListObject有關系嗎?估計到這里可能有人已經懵了,或者說前面章節介紹的已經忘記了。以python中的list為例:

  • PyListObject:對應python中的list實例對象,一個list()就是一個PyListObject結構體實例
  • PyList_Type:就是python中list這個類本身,它被PyListObject中的ob_type域指向

PyListObject支持哪些操作,都會通過ob_type到PyList_Type里面去找。

言歸正傳,我們剛才說,在python解釋器啟動的時候,PyList_Type就已經存在了,並且是全局對象,僅僅再需要小小的完善一下。但是對於自定的class對象A來說(為了解釋方便,假設在底層就叫做PyA_Type吧,當然我們只是假設這么對應的,至於到底是什么我們后面會說,但是目前為了解釋方便就這么叫吧),底層對應的PyA_Type則並不存在,需要申請內存、創建、初始化整個動作序列。所以對於list來說,初始化就只剩下PyType_Ready(也就上面說的小小的完善一下),但是對於自定義的class對象A來說,PyType_Ready僅僅是很小的一部分。

下面我們就以python中的type對象入手,因為它比較特殊。python中的type在底層對應PyType_Type。我們說python中type生成了int、str、dict等內建對象,但是type、object也是內建對象,當然這兩個老鐵的類型也依舊是type。但是在底層,這個所有的內建類型都是一個PyTypeObject對象。

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

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

13.2.1 處理基類和type信息

//typeobject.c
int
PyType_Ready(PyTypeObject *type)
{	
    //這里的參數顯然是PyType_Type
    
    //tp_dict,和繼承的基類,因為是多個所以是bases,當然不用想這些基類也都是PyTypeObject對象
    PyObject *dict, *bases;
    //還是繼承的基類,顯然這個是object,對應PyBaseObject_Type,因為py3中,所有的類都是默認繼承的
    PyTypeObject *base;
    Py_ssize_t i, n;

    /* Initialize tp_base (defaults to BaseObject unless that's us) */
    // 獲取type中tp_base域指定的基類   
    base = type->tp_base;
    if (base == NULL && type != &PyBaseObject_Type) {
        //設置
        base = type->tp_base = &PyBaseObject_Type;
        Py_INCREF(base);
    }

    /* Initialize the base class */
    //如果基類沒有tp_dict,那么會初始化基類
    if (base != NULL && base->tp_dict == NULL) {
        if (PyType_Ready(base) < 0)
            goto error;
    }
	
    //設置type信息
    if (Py_TYPE(type) == NULL && base != NULL)
        Py_TYPE(type) = Py_TYPE(base);

}

python虛擬機會嘗試獲取待初始化的type(PyType_Ready的參數名,這里是PyType_Type)的基類,這個信息是在PyTypeObject.tp_base中指定的,可以看看一些常見內建對象的tp_base信息。

對於指定了tb_base的內建對象,當然就使用指定的基類,而對於沒有指定tp_base的內置class對象,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中就是python的類對象。因此這里是設置這些類對象的ob_type,那么對應的ob_type顯然就是元類metaclass,我們自然會想象到python中的type。但是我們發現Py_TYPE(type) = Py_TYPE(base);這一行代碼是把父類的ob_type設置成了當前類的ob_type,也就是說A類是由XX生成的,那么B在繼承A的時候,B也會由XX生成。這里之所以用XX代替,是因為python中不僅僅是type可以生成類對象,那些繼承了type的子類也可以。

# 必須要繼承自type,否則無法作為一個類的metaclass
class MyType(type):

    def __new__(mcs, name, bases, attrs):
        # 控制類的實例化過程
        # 自動添加一個屬性
        attrs.update({"哈哈": "蛤蛤"})
        return super().__new__(mcs, name, bases, attrs)


# 指定A的metaclass是MyType
class A(metaclass=MyType):
    pass


# 然后讓B去繼承A
# 因為A是由MyType生成的,那么B繼承A之后,B的元類也會是MyType
class B(A):
    pass


print(B.__class__)  # <class '__main__.MyType'>
print(B.哈哈)  # 蛤蛤

所以大家應該明白下面的代碼是做什么的了,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是怎么被初始化的。

13.2.2 處理基類列表

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

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

    /* Initialize tp_base (defaults to BaseObject unless that's us) */
    //獲取tp_base中指定的基類
    base = type->tp_base;
    if (base == NULL && type != &PyBaseObject_Type) {
        base = type->tp_base = &PyBaseObject_Type;
        Py_INCREF(base);
    }
	
    ...
    ...
    ...    

    /* Initialize tp_bases */
    //處理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
            //否則的話,就申請只有一個空間的元素,然后將base(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里面,因此也就出現了這個結果。

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

13.2.3 填充tp_dict

下面python虛擬機就進入了激動人心的tp_dict的填充階段,這是一個極其繁復的過程。

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

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

    /* Add type-specific descriptors to tp_dict */
    //將與type相關的操作加入到tp_dict中
    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;
    }
}

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

13.2.3.1 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中,就存儲着PyTypeObject中一種操作對應的各種信息,比如:int實例對象(PyLongObject)支持哪些操作,就看int(PyTypeObject實例PyLong_Type)支持哪些操作,而PyTypeObject中的一個操作就會有一個slot與之對應。比如slot里面的name就是操作對應的名稱,比如字符串__add__,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)}

TPSLOT和ETSLOT的區別就在於TPSLOT計算的是操作對應的函數指針(比如nb_add)在PyTypeObject中的偏移量,而ETSLOT計算的是函數指針在PyHeapTypeObject中的偏移量,但是我們看一下,PyHeapTypeObject的定義,就能發現端倪

typedef struct _heaptypeobject {
    /* Note: there's a dependency on the order of these members
       in slotptr() in typeobject.c . */
    PyTypeObject ht_type;
    PyAsyncMethods as_async;
    PyNumberMethods as_number;
    PyMappingMethods as_mapping;
    PySequenceMethods as_sequence; /* as_sequence comes after as_mapping,
                                      so that the mapping wins when both
                                      the mapping and the sequence define
                                      a given operator (e.g. __getitem__).
                                      see add_operators() in typeobject.c . */
    PyBufferProcs as_buffer;
    PyObject *ht_name, *ht_slots, *ht_qualname;
    struct _dictkeysobject *ht_cached_keys;
    /* here are optional user slots, followed by the members. */
} PyHeapTypeObject;

我們發現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;
/* Initialize the slotdefs table by adding interned string objects for the
   names. */
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);
        //填充slotdef結構體中的name_strobj
        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;
}

13.2.3.2 從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的調用過程,我們將在后面詳細介紹。

13.2.3.3 建立聯系

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

//typeobject.c
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_GetItem(dict, p->name_strobj))
            continue;
        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中指定的,而且PyTypeObject和PyHeapTypeObject不是同構的,因為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_xxx的過程和add_operators類似,不過最后添加到tp_dict中descriptor就不再是PyWrapperDescrObject,而分別是PyMethodDescrObject、PyMemberDescrObject、PyGetSetDescrObject。

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

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身上。

在slotdefs中,存在:

//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來說,這個變化是在fixup_slot_dispatchers這個函數中完成的,對於內建對象則不會進行此操作。

static void
fixup_slot_dispatchers(PyTypeObject *type)
{
    slotdef *p;

    init_slotdefs();
    for (p = slotdefs; p->name; )
        //遍歷、更新slot
        p = update_one_slot(type, p);
}

13.2.3.4 確定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虛擬機將創建一個tuple對象,里面存放一組類對象,這些對象的順序就是虛擬機確定的mro的順序,最終這個tuple會被保存在PyTypeObject.tp_mro中。

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

對於上圖來說,如果是經典類:顯然是屬性查找是先從A找到I,再從C找到G。而對於新式類,也是同樣的結果,對於上圖這種繼承結構,至於兩邊是否一樣多則不重要,經典類和新式類是一樣的。我們先看結論,我們下面顯示的都只是新式類。

# 這里是python3.7 新式類
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,所以第一張圖中,即使我們沒畫完,但是也能想到,兩條涇渭分明的繼承鏈的上方最終應該都指向class 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算法比較復雜,涉及到拓撲學,如果再去研究拓撲學就本末倒置了。因此,對於python的多繼承來說,我只希望記住一句不是很好理解的話:當沿着一條繼承鏈尋找類時,如果這個類出現在了另一條繼承鏈當中,那么當前的繼承鏈的搜索就會結束,然后在"最開始"出現分歧的地方轉向下一條繼承鏈的搜索。這是我個人總結的,或許比較難理解,但是通過例子就能明白了。

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

  • 首先最開始的順序是A,如果我們獲取A的mro的話
  • 然后A繼承B和C,由於是兩條路,因此我們說A這里就是一個分歧點,但是由於B在前,所以接下來是B。現在mro的順序是A B
  • 但是B這里也出現了分歧點,不過不用管,我們關注的是最開始出現分歧的地方。現在mro的順序是A B D
  • 然后從D開始尋找,這里注意了,按理說會找G的,但是G不止被一個人繼承,也就是意味着沿着當前的繼承鏈查找G時,G還出現在了其它的繼承鏈當中。怎么辦?顯然要回到最初的分歧點,轉向下一條繼承鏈的搜索
  • 最初的分歧點是A,那么該去找C了,現在mro的順序就是A B D C
  • 注意C這里出現了分歧點,而A的分支已經結束了,所以現在C就是最初的分歧點了。而C繼承自E和F,顯然要搜索E,那么此時mro的順序就是A B D C E
  • 然后從E開始搜索,顯然要搜索G,此時mro順序是A B D C E G
  • 從G要搜索I,注意這里I可沒有被H繼承哦。此時mro順序是A B D C E G I
  • 從I開始搜索誰呢?J顯然出現在了其它的繼承鏈中,那么要回到最初分歧的地方,也就是C,那么下面顯然要找F,此時mro順序是A B D C E G I F
  • F只繼承了H,那么肯定要找H,此時mro順序是 A B D C E G I F H
  • 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'>
"""

我們再看一個例子

  • 首先是A,A繼承B1,B2,B3,會先走B1,此時mro是A B1,注意現在A是分歧點
  • 從B1本來該找C1,但是C1還被其他類繼承,也就是出現在了其它的繼承鏈當中,因此要回到最初分歧點A,從下一條繼承鏈開始找,顯然要找B2,此時mro就是A B1 B2
  • 從B2開始,顯然要找C1,此時mro順序就是A B1 B2 C1
  • 從C1開始,顯然要找D1,因為D1只被C1繼承,此時mro順序是A B1 B2 C1 D1
  • 從D1顯然不會找E的,咋辦,回到最初的分歧點,注意這里顯然還是A,因為A的分支還沒有走完。顯然此時要走B3,那么mro順序就是A B1 B2 C1 D1 B3
  • 從B3開始找,顯然要找C2,注意:A的分支已經走完,此時B3就成了新的最初分歧點。現在mro順序是A B1 B2 C1 D1 B3 C2
  • C2會找D2嗎?顯然不會,因為它還被C3繼承,所以它出現在了其他的繼承鏈中。所以要回到最初分歧點,這里是B3,顯然下面要找C3,另外由於B3的分支也已經走完,所以現在C3就成了新的最初分歧點。此時mro順序是A B1 B2 C1 D1 B3 C2 C3
  • 從C3開始,顯然要找D2,此時mro順序是A B1 B2 C1 D1 B3 C2 C3 D2
  • 但是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'>
"""

底層源碼我們就不再看了,個人覺得從目前這個層面來理解已經足夠了。

13.2.3.5 繼承基類操作

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

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

//typeobject.c
int
PyType_Ready(PyTypeObject *type)
{
    PyObject *dict, *bases;
    PyTypeObject *base;
    Py_ssize_t i, n;
	
    ...
    ...    
    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);
    ...
    ...    
}

我們在里面看到很多熟悉的東西,如果你常用魔法方法的話。而且我們知道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中,整型是可以和布爾類型進行運算的,看似不可思議,但又在情理之中。

不過下面有一個例子,想一想為什么是這個結果

d = {1: "aaa"}
d[True] = "bbb"
print(d)  # {1: 'bbb'}

我們說True在被當成int的時候會被解釋成1,而原來的字典里面已經有1這個key了,所以此時True和1是等價的。原來的1還是1,d[True]等價於d[1],所以value被換成了"bbb"

d = {1: "aaa1", False: "bbb1"}

d[True] = "aaa2"
d[0] = "bbb2"
print(d)  # {1: 'aaa2', False: 'bbb2'}

d[1.0] = "aaa3"
d[0.0] = "bbb3"
print(d)  # {1: 'aaa3', False: 'bbb3'}

可見,對於字典的存儲來說,True、1、1.0三者等價,False、0、0.0三者等價。

d = {1: "a", True: "b", 1.0: "c"}
print(d)  # {1: 'c'}
# 盡管等價,但是key就是最先出現的key,比如這里先出現的是1,那么后續只會把1對應的value換掉,而不會換掉1這個key

13.2.3.6 填充基類中的子類列表

到這里,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的操作。

13.3 自定義class

下面就對用戶自定義的class、也就是我們最開始分類的class對象的剖析。

# a.py
class A:

    name = "Python"

    def __init__(self):
        print("A->__init__")

    def f(self):
        print("A->self")

    def g(self, value):
        self.value = value
        print(self.value)


a = A()
a.f()
a.g(10)
"""
A->__init__
A->self
10
"""

通過之前對函數機制的分析中,我們知道,對於一個包含函數定義的python源文件,在編譯之后,會得到一個和源文件對應的PyCodeObject對象,假設叫module,而與函數對應的PyCodeObject對象,假設叫func,那么func顯然存儲在module中。那么對於包含類的python源文件,編譯之后的結果又是怎么樣的呢?

我們可以照葫蘆畫瓢,根據以前的經驗,推測a.py對應的PyCodeObject對象包含與class對應的PyCodeObject,而與class對應的PyCodeObject會包含3個與函數對應的PyCodeObject,然而事實正是如此。

在介紹函數的時候,我們看到函數的聲明、def語句和函數的實現代碼雖然是一個邏輯整體,但是它們的字節碼指令確實分離在兩個PyCodeObject對象中的。在類中,同樣存在這樣的分離現象。聲明類的class語句,編譯后的字節碼指令存儲在模塊對應的PyCodeObject中,而類的實現、也就是類里面的邏輯,編譯后的字節碼指令序列則存儲在類對應的的PyCodeObject中。所以我們在模塊級別中只能找到類,無法直接找到類里面的成員。

另外還可以看到,類的成員函數和一般的函數相同,也會有這種聲明和實現分離的現象。其實也很好理解,就把類和函數想象成變量就行了,類名、函數名就是變量名,而類、函數里面的邏輯想象成值,一個變量對應一個值。

13.3.1 class對象的動態元信息

class對應(class關鍵字創建的類)的元信息指的就是關於class的信息,比如說class的名稱、它所擁有的的屬性、方法,該class實例化時要為實例對象申請的內存空間大小等。對於a.py中定義的class A來說,我們必須知道這樣的信息:在class A中,有一個符號f,這個f對應一個函數;還有一個符號g,這個g也對應了一個函數。有了這些關於A的元信息,才能創建class對象,否則我們是沒辦法創建的。元信息是一個非常重要的概念,比如說hive,數據的元信息就是存儲在mysql里面的,而在編程語言中,正是通過元信息才實現了反射等動態特性。而在python中,元信息的概念被發揮的淋漓盡致,因此python也提供了其他編程語言所不具備的高度靈活的動態特征。

下面還是老規矩,查看a.py的字節碼

class A(object):

    name = "Python"

    def __init__(self):
        print("A->__init__")

    def f(self):
        print("A->self")

    def g(self, value):
        self.value = value
        print(self.value)
1           0 LOAD_BUILD_CLASS
            2 LOAD_CONST               0 (<code object A at 0x0000027F75147C90, file "a.py", line 1>)
            4 LOAD_CONST               1 ('A')
            6 MAKE_FUNCTION            0
            8 LOAD_CONST               1 ('A')
           10 LOAD_NAME                0 (object)
           12 CALL_FUNCTION            3
           14 STORE_NAME               0 (A)
           16 LOAD_CONST               2 (None)
           18 RETURN_VALUE

Disassembly of <code object A at 0x0000027F75147C90, file "a.py", line 1>:
1           0 LOAD_NAME                0 (__name__)
            2 STORE_NAME               1 (__module__)
            4 LOAD_CONST               0 ('A')
            6 STORE_NAME               2 (__qualname__)

3           8 LOAD_CONST               1 ('Python')
           10 STORE_NAME               3 (name)

5          12 LOAD_CONST               2 (<code object __init__ at 0x0000027F750DA870, file "a.py", line 5>)
           14 LOAD_CONST               3 ('A.__init__')
           16 MAKE_FUNCTION            0
           18 STORE_NAME               4 (__init__)

8          20 LOAD_CONST               4 (<code object f at 0x0000027F75145190, file "a.py", line 8>)
           22 LOAD_CONST               5 ('A.f')
           24 MAKE_FUNCTION            0
           26 STORE_NAME               5 (f)

11         28 LOAD_CONST               6 (<code object g at 0x0000027F751457C0, file "a.py", line 11>)
           30 LOAD_CONST               7 ('A.g')
           32 MAKE_FUNCTION            0
           34 STORE_NAME               6 (g)
           36 LOAD_CONST               8 (None)
           38 RETURN_VALUE

Disassembly of <code object __init__ at 0x0000027F750DA870, file "a.py", line 5>:
6           0 LOAD_GLOBAL              0 (print)
            2 LOAD_CONST               1 ('A->__init__')
            4 CALL_FUNCTION            1
            6 POP_TOP
            8 LOAD_CONST               0 (None)
           10 RETURN_VALUE

Disassembly of <code object f at 0x0000027F75145190, file "a.py", line 8>:
9           0 LOAD_GLOBAL              0 (print)
            2 LOAD_CONST               1 ('A->self')
            4 CALL_FUNCTION            1
            6 POP_TOP
            8 LOAD_CONST               0 (None)
           10 RETURN_VALUE

Disassembly of <code object g at 0x0000027F751457C0, file "a.py", line 11>:
12          0 LOAD_FAST                1 (value)
            2 LOAD_FAST                0 (self)
            4 STORE_ATTR               0 (value)

13          6 LOAD_GLOBAL              1 (print)
            8 LOAD_FAST                0 (self)
           10 LOAD_ATTR                0 (value)
           12 CALL_FUNCTION            1
           14 POP_TOP
           16 LOAD_CONST               0 (None)
           18 RETURN_VALUE

字節碼比較長,我們逐行分析,當然很多字節碼我們都見過了,因此有的字節碼介紹的時候就不會特別詳細了。

我們仔細觀察一下字節碼,發現分為五個部分,模塊的字節碼、class的字節碼、class的三個函數的字節碼。

我們先來看看模塊的字節碼

1           0 LOAD_BUILD_CLASS
            2 LOAD_CONST               0 (<code object A at 0x0000027F75147C90, file "a.py", line 1>)
            4 LOAD_CONST               1 ('A')
            6 MAKE_FUNCTION            0
            8 LOAD_CONST               1 ('A')
           10 LOAD_NAME                0 (object)
           12 CALL_FUNCTION            3
           14 STORE_NAME               0 (A)
           16 LOAD_CONST               2 (None)
           18 RETURN_VALUE

0 LOAD_BUILD_CLASS,我們注意到這又是一條我們沒見過的新指令。從名字也能看出來這是要構建一個類,其實對於A這個類來說,也是要先MAKE_FUNCTION的,但是要轉化為類,所以要LOAD_BUILD_CLASS

//ceval.c
        TARGET(LOAD_BUILD_CLASS) {
            _Py_IDENTIFIER(__build_class__);

            PyObject *bc;
            if (PyDict_CheckExact(f->f_builtins)) {
                //從f_builtins里面獲取PyId___build_class__
                bc = _PyDict_GetItemId(f->f_builtins, &PyId___build_class__);
                if (bc == NULL) {
                    PyErr_SetString(PyExc_NameError,
                                    "__build_class__ not found");
                    goto error;
                }
                Py_INCREF(bc);
            }
            else {
                PyObject *build_class_str = _PyUnicode_FromId(&PyId___build_class__);
                if (build_class_str == NULL)
                    goto error;
                bc = PyObject_GetItem(f->f_builtins, build_class_str);
                if (bc == NULL) {
                    if (PyErr_ExceptionMatches(PyExc_KeyError))
                        PyErr_SetString(PyExc_NameError,
                                        "__build_class__ not found");
                    goto error;
                }
            }
            //入棧
            PUSH(bc);
            DISPATCH();
        }

LOAD_BUILD_CLASS是從python的內置函數中取得__build_class__將其入棧,然后下面的幾個指令很好理解,但是卻出現了一個CALL_FUNCTION是做什么的,我們目前還沒有調用啊,其實這個CALL_FUNCTION使用來生成類的,我們看到它的參數個數是3個,分別是:A的PyFunctionObject、A、__build_class_class__,因此:

class A(object):
    pass


# 在底層將會被翻譯成
A = __build_class__(<PyFunctionObject A>, A, object)
# 所以我們發現這個函數至少需要兩個參數
import sys
# 在python中我們需要使用builtins模塊來導入,才能使用__build_class__
import builtins
try:
    builtins.__build_class__()
except Exception as e:
    exc_type, exc_value, _ = sys.exc_info()
    print(exc_type, exc_value)  # class 'TypeError'> __build_class__: not enough arguments

try:
    builtins.__build_class__("", "")
except Exception as e:
    exc_type, exc_value, _ = sys.exc_info()
    print(exc_type, exc_value)  # <class 'TypeError'> __build_class__: func must be a function

try:
    builtins.__build_class__(lambda: 123, 123)
except Exception as e:
    exc_type, exc_value, _ = sys.exc_info()
    print(exc_type, exc_value)  # <class 'TypeError'> __build_class__: name is not a string

但是報了三種錯誤,記住這幾個報錯信息,后面馬上就會看到。

所以現在就明白為什么會出現CALL_FUNCTION這條指令,__build_class__就是用來將一個函數對象變成一個class對象。其實這個過程有點像閉包,如果把class想象成def的話,那么是可以當成閉包的,而這個__build_class__正是將閉包函數build成class之后,通過實例調用內部方法(閉包)時,會將實例本身傳給方法(閉包)的第一個參數。

class的字節碼

Disassembly of <code object A at 0x0000027F75147C90, file "a.py", line 1>:
1           0 LOAD_NAME                0 (__name__)
            2 STORE_NAME               1 (__module__)
            4 LOAD_CONST               0 ('A')
            6 STORE_NAME               2 (__qualname__)

3           8 LOAD_CONST               1 ('Python')
           10 STORE_NAME               3 (name)

5          12 LOAD_CONST               2 (<code object __init__ at 0x0000027F750DA870, file "a.py", line 5>)
           14 LOAD_CONST               3 ('A.__init__')
           16 MAKE_FUNCTION            0
           18 STORE_NAME               4 (__init__)

8          20 LOAD_CONST               4 (<code object f at 0x0000027F75145190, file "a.py", line 8>)
           22 LOAD_CONST               5 ('A.f')
           24 MAKE_FUNCTION            0
           26 STORE_NAME               5 (f)

11         28 LOAD_CONST               6 (<code object g at 0x0000027F751457C0, file "a.py", line 11>)
           30 LOAD_CONST               7 ('A.g')
           32 MAKE_FUNCTION            0
           34 STORE_NAME               6 (g)
           36 LOAD_CONST               8 (None)
           38 RETURN_VALUE

這里插一嘴,有些人可能對這個字節碼順序不是很理解,對於dis.dis來說,是先對整個模塊、然后是類和變量和函數等等。比如:這里的創建類,執行模塊的字節碼,只是提示要build一個類,當我們調用的時候,也只是告訴我們要進行調用。對於類的字節碼,只是提示創建了幾個函數,但是函數的細節並沒有說,然后函數的字節碼,才是真正解析里面的邏輯。因此一個模塊里面有個類,類里面有一個函數,函數有里面寫邏輯,那么這個字節碼會分成三部分。模塊的字節碼,dis.dis會告訴我們創建了一個類,解釋類的字節碼,dis.dis會告訴我們創建一個函數,調用也僅僅是LOAD_CONST、CALL_FUNCTION,告訴我們函數調用了。解釋函數的字節碼的時候,才會告訴我們函數的執行邏輯。如果函數里面還有個閉包,那么閉包的邏輯也是不會告訴我們,僅僅是說創建了一個閉包,只有執行閉包的字節碼的時候,dis.dis會告訴我們閉包里面的邏輯是如何執行的。說的很繞口,但是dis.dis打印出的字節碼的執行流程是分層級的,個人覺得這樣的展示方式是很合理的,很清晰。

對於class A的字節碼。開始的LOAD_NAME和STORE_NAME是將符號__module__和全局命名空間中符號__name___的值(__main_)關聯了起來,並放入到local命名空間(PyFrameObject的f_locals)中。需要說明的是,我們在介紹函數的時候提過,當時我們說:"函數的局部變量是不可變的,在編譯的時候就已經確定了,是以一種靜態方式放在了運行時棧前面的那段內存中,並沒有放在f_locals中,f_locals其實是一個NULL,我們通過locals()拿到的只是對運行時棧前面的內存的一個拷貝,函數里面的局部變量是通過靜態方式來訪問的。而類就不一樣了,類是可以動態修改的,可以隨時增加屬性、方法,這就意味着類是不可能通過靜態方式來查找屬性的"。而事實上也確實如此,類也有一個f_locals,但它指向的就不再是NULL了,而和f_globals一樣,也是一個PyDictObject對象。然后是LOAD_CONST,將字符串"A"load進來,和__qualname__組成一個entry存儲在A的locals中。

class Cls:
    pass


# 將__name__ load進來,對於當前模塊來說就是__main__
print(__name__)  # __main__
# 然后和Cls中的locals中的__module__關聯起來,所以這兩者是一樣的
print(Cls.__module__)  # __main__

# 將字符串Cls load進來,和__qualname__關聯起來
# 從字節碼的分析,顯然是這個結果
print(Cls.__qualname__)  #Cls

下面的字節碼就不用分析了,看過之前的章節的話,會清楚在干什么。連續執行了三個(LOAD_CONST、MAKE_FUNCTION、STORE_NAME),顯然每個指令序列都會創建一個與類中成員函數對應的PyFunctionObject對象,並將函數名和其對應的PyFunctionObject對象通過STORE_NAME指令存入到local命名空間中。

到了這時,我們看到已經創建了很多東西。這些東西就是class A的元信息,被放到了local命名空間中,准確的說是動態元信息,既然有動態元信息,那就有靜態元信息,關於這兩者的區別我們后面介紹。

13.3.2 metaclass

我們說LOAD_BUILD_CLASS是將一個PyFunctionObject變成一個類,盡管它寫在最前面,但實際上是需要將class A對應的PyCodeObject對象包裝成一個PyFunctionObject對象之后才能執行。我們說__build_class__是用來將PyFunctionObject變成類的函數,我們來看看它長什么樣子

//python/bltinmodule.c
static PyMethodDef builtin_methods[] = {
    {"__build_class__", (PyCFunction)builtin___build_class__,
     METH_FASTCALL | METH_KEYWORDS, build_class_doc},
    ...
    ...
}        
    
static PyObject *
builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
                        PyObject *kwnames)
{
    PyObject *func, *name, *bases, *mkw, *meta, *winner, *prep, *ns, *orig_bases;
    PyObject *cls = NULL, *cell = NULL;
    int isclass = 0;   /* initialize to prevent gcc warning */
	
    //我們說了底層調用的是builtin___build_class__
    //class A: 會被翻譯成builtin.__build_class__(PyFunctionObject, "class name")
    //所以這個函數只要需要兩個參數
    if (nargs < 2) {
        //參數不足,報錯,還記的這個報錯信息嗎?上面測試過的
        PyErr_SetString(PyExc_TypeError,
                        "__build_class__: not enough arguments");
        return NULL;
    }
    
    //類對應的PyFunctionObject
    func = args[0];   /* Better be callable */
    if (!PyFunction_Check(func)) {
        //如果不是PyFunctionObject,報錯,這個信息有印象嗎?
        PyErr_SetString(PyExc_TypeError,
                        "__build_class__: func must be a function");
        return NULL;
    }
    
    //類對應的名字,__build_class__的時候 總要給類起一個名字吧
    name = args[1];
    if (!PyUnicode_Check(name)) {
        //如果不是一個PyUnicodeObject,報錯,這個有印象嗎?
        PyErr_SetString(PyExc_TypeError,
                        "__build_class__: name is not a string");
        return NULL;
    }
    
    //原始基類
    orig_bases = _PyStack_AsTupleSlice(args, nargs, 2, nargs);
    if (orig_bases == NULL)
        return NULL;
	
    //獲取class的基類列表
    bases = update_bases(orig_bases, args + 2, nargs - 2);
    if (bases == NULL) {
        Py_DECREF(orig_bases);
        return NULL;
    }
	
    //創建時kwnames為NULL
    if (kwnames == NULL) {
        meta = NULL;
        mkw = NULL;
    }
    else {
        mkw = _PyStack_AsDict(args + nargs, kwnames);
        if (mkw == NULL) {
            Py_DECREF(bases);
            return NULL;
        }
		
        //這里獲取meta
        meta = _PyDict_GetItemId(mkw, &PyId_metaclass);
        if (meta != NULL) {
            Py_INCREF(meta);
            if (_PyDict_DelItemId(mkw, &PyId_metaclass) < 0) {
                Py_DECREF(meta);
                Py_DECREF(mkw);
                Py_DECREF(bases);
                return NULL;
            }
            /* metaclass is explicitly given, check if it's indeed a class */
            isclass = PyType_Check(meta);
        }
    }
    //如果meta為NULL,這意味着用戶沒有指定metaclass
    if (meta == NULL) {
        /* if there are no bases, use type: */
        //然后嘗試獲取基類,如果沒有基類
        if (PyTuple_GET_SIZE(bases) == 0) {
            //指定metaclass為type
            meta = (PyObject *) (&PyType_Type);
        }
        /* else get the type of the first base */
        //否則使用第一個繼承的基類的metaclass
        else {
            PyObject *base0 = PyTuple_GET_ITEM(bases, 0); //拿到第一基類
            meta = (PyObject *) (base0->ob_type); //拿到第一基類的__class__
        }
        Py_INCREF(meta);
        //meta也是一個類
        isclass = 1;  /* meta is really a class */
    }
	
    //如果isclass
    if (isclass) {
        /* meta is really a class, so check for a more derived
           metaclass, or possible metaclass conflicts: */
        //元類也是類
        //所以winner這里就是bases[0].__class__
        winner = (PyObject *)_PyType_CalculateMetaclass((PyTypeObject *)meta,
                                                        bases);
        if (winner == NULL) {
            Py_DECREF(meta);
            Py_XDECREF(mkw);
            Py_DECREF(bases);
            return NULL;
        }
        if (winner != meta) {
            Py_DECREF(meta);
            meta = winner;
            Py_INCREF(meta);
        }
    }
    /* else: meta is not a class, so we cannot do the metaclass
       calculation, so we will use the explicitly given object as it is */
    //根據上面的邏輯,我們知道隱含了一個else,由於meta不是一個類,這就意味着無法使用metaclass的邏輯
    //會顯式地指定一個對象
    //__prepare__方法
    if (_PyObject_LookupAttrId(meta, &PyId___prepare__, &prep) < 0) {
        ns = NULL;
    }
    //這個__prepare__方法必須返回一個字典,如果返回None,那么默認返回一個孔子點
    else if (prep == NULL) {
        ns = PyDict_New();
    }
    else {
        //否則將字典返回
        PyObject *pargs[2] = {name, bases};
        ns = _PyObject_FastCallDict(prep, pargs, 2, mkw);
        Py_DECREF(prep);
    }
    if (ns == NULL) {
        Py_DECREF(meta);
        Py_XDECREF(mkw);
        Py_DECREF(bases);
        return NULL;
    }
    //如果返回的不是一個字典,那么報錯,我們來演示一下
    if (!PyMapping_Check(ns)) {
        PyErr_Format(PyExc_TypeError,
                     "%.200s.__prepare__() must return a mapping, not %.200s",
                     isclass ? ((PyTypeObject *)meta)->tp_name : "<metaclass>",
                     Py_TYPE(ns)->tp_name);
    /*
    class Meta(type):
        @classmethod
        def __prepare__(mcs, name, bases):
            return 123
            
    class A(metaclass=Meta):
        pass
    """
    TypeError: Meta.__prepare__() must return a mapping, not int
    """
    */
        goto error;
    }
    cell = PyEval_EvalCodeEx(PyFunction_GET_CODE(func), PyFunction_GET_GLOBALS(func), ns,
                             NULL, 0, NULL, 0, NULL, 0, NULL,
                             PyFunction_GET_CLOSURE(func));
    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_FastCallDict(meta, margs, 3, mkw);
    ...
    ...
}   

我們前面說,python虛擬機獲得了關於class的屬性表(動態元信息),比如所有的方法、屬性,所以我們可以說,class的動態元信息包含了class的所有屬性。但是對於這個class對象的類型是什么,應該如何創建、要分配多少內存,卻沒有任何的信息。而在builtin___build_class__中,metaclass正是關於class對象的另一部分元信息,我們稱之為靜態元信息。在靜態元信息中,隱藏着所有的類對象應該如何創建的信息,注意:是所有的類對象。

從源碼中我們可以看到,如果用戶指定了metaclass,那么會選擇指定的metaclass,如果沒有指定,那么會使用第一個繼承的基類的__class__作為該class的metaclass。

對於PyLongObject、PyDictObject這些python中的實例對象,所有的元信息存儲在對應的類對象中(PyLong_Type,PyDict_Type)。但是對於類對象來說,其元信息的靜態元信息存儲在對應的元類(PyType_Type)中,動態元信息則存儲在本身的local命名空間中。但是為什么這么做呢?為什么對於類對象來說,其元信息要游離成兩部分呢?都存在metaclass里面不香嗎?這是因為,用戶在.py文件中可以定義不同的class,這個元信息必須、且只能是動態的,所以它是不適合保存在metaclass中的,而類對象的創建策略等這些所有class都會共用的元信息,則存儲在metaclass里面。

像Python的內建對象都是python靜態提供的,它們都具備相同的接口集合,支持什么操作一開始就定義好了。只不過有的可以用,有的不能用。比如PyLongObject可以使用nb_add,但是PyDictObject不能。而PyDictObject可以使用mp_subscript,但是PyLongObject不可以。盡管如此,但這不影響它們的所有元信息都可以完全存儲在類型對象中。但是用戶自定義的class對象,接口是動態的,不可能再metaclass中靜態指定

既然創建了元類,那么下面顯然就開始調用了。通過函數_PyObject_FastCallDict調用

//Objects/call.c
PyObject *
_PyObject_FastCallDict(PyObject *callable, PyObject *const *args, Py_ssize_t nargs,
                       PyObject *kwargs)
{
    ...
    ...    
    else {
        PyObject *argstuple, *result;
        ternaryfunc call;

        /* Slow-path: build a temporary tuple */
        //調用了tp_call,指向type_call
        call = callable->ob_type->tp_call;
        result = (*call)(callable, argstuple, kwargs);
        ...
        ...    
        return result;
    }
}

//Objects/typeobject.c
static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    PyObject *obj;
	
    ...
    //調用tp_new進行初始化
    obj = type->tp_new(type, args, kwds);
    ...
    type = Py_TYPE(obj);
    if (type->tp_init != NULL) {
        //如果定義了__init__函數,那么會調用__init__函數進行初始化
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            assert(PyErr_Occurred());
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!PyErr_Occurred());
        }
    }
    return obj;
}

tp_new指向type_new,這個type_new是我們創建class對象的第一案發現場。我們看一下type_new的源碼,位於Objects/typeobject.c中,這個函數的代碼比較長,我們會有刪減,像那些檢測的代碼我們就省略掉了。

static PyObject *
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{	
    //都是類的那些動態元信息
    PyObject *name, *bases = NULL, *orig_dict, *dict = NULL;
    PyObject *qualname, *slots = NULL, *tmp, *newslots, *cell;
    PyTypeObject *type = NULL, *base, *tmptype, *winner;
    PyHeapTypeObject *et;
    PyMemberDef *mp;
    Py_ssize_t i, nbases, nslots, slotoffset, name_size;
    int j, may_add_dict, may_add_weak, add_dict, add_weak;
	
    //如果metaclass是type的話
    if (metatype == &PyType_Type) {
        //獲取位置參數和關鍵字參數個數
        const Py_ssize_t nargs = PyTuple_GET_SIZE(args);
        const Py_ssize_t nkwds = kwds == NULL ? 0 : PyDict_GET_SIZE(kwds);
		
        //位置參數為1,關鍵字參數為0,你想到了什么
        //type(xxx),是不是這個呀,
        if (nargs == 1 && nkwds == 0) {
            PyObject *x = PyTuple_GET_ITEM(args, 0);
            Py_INCREF(Py_TYPE(x));
            //這顯然是初學python的時候,就知道的,查看一個變量的類型。
            //直接返回
            return (PyObject *) Py_TYPE(x);
        }
		
        //如果上面的if不滿足,會走這里,表示現在不再是查看類型了,而是創建類
        //而這里要求位置參數必須是3個,否則報錯。
        //我們知道type查看類型,輸出一個參數即可,但是創建類需要3個
        if (nargs != 3) {
            //不過這里是nargs,似乎跟nkwds沒有關系啊,我們用python來測試一下
            //type(name="xx", bases=(object, ), dict={})  #TypeError: type() takes 1 or 3 arguments
            //type("xx", (object, ), {}),此時正常執行。
            //說明關鍵字參數不管用,只能通過位置參數來傳遞,從源碼中我們能看到端倪
            PyErr_SetString(PyExc_TypeError,
                            "type() takes 1 or 3 arguments");
            return NULL;
        }
    }

    //現在顯然是確定參數類型,對於type來說,你傳遞了三個參數,但是這三個參數是有類型要求的
    //必須是PyUnicodeObject、PyTupleObject、PyDictObject
    /*
    type(123, (object, ), {})  # TypeError: type.__new__() argument 1 must be str, not int
	type("xx", [object], {})  # TypeError: type.__new__() argument 2 must be tuple, not list
	type("xx", (object, ), [])  # TypeError: type.__new__() argument 3 must be dict, not list
    */
    if (!PyArg_ParseTuple(args, "UO!O!:type.__new__", &name, &PyTuple_Type,
                          &bases, &PyDict_Type, &orig_dict))
        return NULL;

    /* Adjust for empty tuple bases */
    //處理元組為空的情況,另外我們使用class關鍵字定義類,本質上會轉為type定義類的方式
    /*
    class A:
    	pass
    這種形式本質上就是type("A", (), {})
    */
    nbases = PyTuple_GET_SIZE(bases);
    //但是我們發現這里沒有繼承基類,因此在python3中要處理這種情況,默認繼承object
    if (nbases == 0) {
        //拿到PyBaseObject_Type,也就是python中的object
        base = &PyBaseObject_Type;
        //生成一個只有base的元組
        bases = PyTuple_Pack(1, base);
        if (bases == NULL)
            return NULL;
        //nbases顯然是1
        nbases = 1;
    }
    else {
        _Py_IDENTIFIER(__mro_entries__);
        //循環遍歷bases的所有元素
        for (i = 0; i < nbases; i++) {
            //依次遍歷bases中的每一個元素
            tmp = PyTuple_GET_ITEM(bases, i);
            //如果是PyType_Type類型,進行下一次循環
            if (PyType_Check(tmp)) {
                continue;
            }
            if (_PyObject_LookupAttrId(tmp, &PyId___mro_entries__, &tmp) < 0) {
                return NULL;
            }
            if (tmp != NULL) {
                PyErr_SetString(PyExc_TypeError,
                                "type() doesn't support MRO entry resolution; "
                                "use types.new_class()");
                Py_DECREF(tmp);
                return NULL;
            }
        }
        /* Search the bases for the proper metatype to deal with this: */
        //尋找父類的metaclass
        winner = _PyType_CalculateMetaclass(metatype, bases);
        if (winner == NULL) {
            return NULL;
        }
		
        //如果winner表示PyType_Type
        if (winner != metatype) {
            if (winner->tp_new != type_new) /* Pass it to the winner */
                return winner->tp_new(winner, args, kwds);
            metatype = winner;
        }

        /* Calculate best base, and check that all bases are type objects */
        //確定最佳base,存儲在PyTypeObject *base中
        base = best_base(bases);
        if (base == NULL) {
            return NULL;
        }

        Py_INCREF(bases);
    }


    /* Check for a __slots__ sequence variable in dict, and count it */
    //處理用戶定義了__slots__屬性的邏輯
    slots = _PyDict_GetItemId(dict, &PyId___slots__);
    nslots = 0;
    add_dict = 0;
    add_weak = 0;
    may_add_dict = base->tp_dictoffset == 0;
    may_add_weak = base->tp_weaklistoffset == 0 && base->tp_itemsize == 0;
    if (slots == NULL) {
    ...
    ...
    ...
        
    /* Allocate the type object */
    //為class對象申請內存
    type = (PyTypeObject *)metatype->tp_alloc(metatype, nslots);
    if (type == NULL)
        goto error;

    /* Keep name and slots alive in the extended type object */
    et = (PyHeapTypeObject *)type;
    Py_INCREF(name);
    et->ht_name = name;
    et->ht_slots = slots;
    slots = NULL;

    /* 初始化tp_flags */
    type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HEAPTYPE |
        Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_FINALIZE;
    if (base->tp_flags & Py_TPFLAGS_HAVE_GC)
        type->tp_flags |= Py_TPFLAGS_HAVE_GC;

    //設置PyTypeObject中的各個域
    type->tp_as_async = &et->as_async;
    type->tp_as_number = &et->as_number;
    type->tp_as_sequence = &et->as_sequence;
    type->tp_as_mapping = &et->as_mapping;
    type->tp_as_buffer = &et->as_buffer;
    type->tp_name = PyUnicode_AsUTF8AndSize(name, &name_size);
    if (!type->tp_name)
        goto error;
    if (strlen(type->tp_name) != (size_t)name_size) {
        PyErr_SetString(PyExc_ValueError,
                        "type name must not contain null characters");
        goto error;
    }

    /* 設置基類和基類列表 */
    type->tp_bases = bases;
    bases = NULL;
    Py_INCREF(base);
    type->tp_base = base;

    /* 設置屬性表 */
    Py_INCREF(dict);
    type->tp_dict = dict;

    /* Set __module__ in the dict */
    //設置__module__
    if (_PyDict_GetItemId(dict, &PyId___module__) == NULL) {
        tmp = PyEval_GetGlobals();
        if (tmp != NULL) {
            tmp = _PyDict_GetItemId(tmp, &PyId___name__);
            if (tmp != NULL) {
                if (_PyDict_SetItemId(dict, &PyId___module__,
                                      tmp) < 0)
                    goto error;
            }
        }
    }

    /* Set ht_qualname to dict['__qualname__'] if available, else to
       __name__.  The __qualname__ accessor will look for ht_qualname.
    */
    //設置__qualname__,並且注釋也寫了,這和__name__是一樣的
    qualname = _PyDict_GetItemId(dict, &PyId___qualname__);
    if (qualname != NULL) {
        if (!PyUnicode_Check(qualname)) {
            PyErr_Format(PyExc_TypeError,
                         "type __qualname__ must be a str, not %s",
                         Py_TYPE(qualname)->tp_name);
            goto error;
        }
    }
    et->ht_qualname = qualname ? qualname : et->ht_name;
    Py_INCREF(et->ht_qualname);
    if (qualname != NULL && _PyDict_DelItemId(dict, &PyId___qualname__) < 0)
        goto error;

    /* Set tp_doc to a copy of dict['__doc__'], if the latter is there
       and is a string.  The __doc__ accessor will first look for tp_doc;
       if that fails, it will still look into __dict__.
    */
    {
        PyObject *doc = _PyDict_GetItemId(dict, &PyId___doc__);
        if (doc != NULL && PyUnicode_Check(doc)) {
            Py_ssize_t len;
            const char *doc_str;
            char *tp_doc;

            doc_str = PyUnicode_AsUTF8(doc);
            if (doc_str == NULL)
                goto error;
            /* Silently truncate the docstring if it contains null bytes. */
            len = strlen(doc_str);
            tp_doc = (char *)PyObject_MALLOC(len + 1);
            if (tp_doc == NULL) {
                PyErr_NoMemory();
                goto error;
            }
            memcpy(tp_doc, doc_str, len + 1);
            type->tp_doc = tp_doc;
        }
    }

    /* Special-case __new__: if it's a plain function,
       make it a static function */
    //如果自定義的class中重寫了__new__方法,將__new__對應的函數改造為static函數
    tmp = _PyDict_GetItemId(dict, &PyId___new__);
    if (tmp != NULL && PyFunction_Check(tmp)) {
        tmp = PyStaticMethod_New(tmp);
        if (tmp == NULL)
            goto error;
        if (_PyDict_SetItemId(dict, &PyId___new__, tmp) < 0) {
            Py_DECREF(tmp);
            goto error;
        }
        Py_DECREF(tmp);
    }

    /* Special-case __init_subclass__ and __class_getitem__:
       if they are plain functions, make them classmethods */
    //設置__init_subclass__,如果子類繼承了父類,那么會觸發父類的__init_subclass__方法
    //用法可以參考我的這篇博客:https://www.cnblogs.com/traditional/p/11715511.html
    tmp = _PyDict_GetItemId(dict, &PyId___init_subclass__);
    if (tmp != NULL && PyFunction_Check(tmp)) {
        tmp = PyClassMethod_New(tmp);
        if (tmp == NULL)
            goto error;
        if (_PyDict_SetItemId(dict, &PyId___init_subclass__, tmp) < 0) {
            Py_DECREF(tmp);
            goto error;
        }
        Py_DECREF(tmp);
    }
	
    //設置__class_getitem__,這個是什么?類似於__getitem__
    //__class_getitem__支持通過類["xxx"]的方式訪問
    tmp = _PyDict_GetItemId(dict, &PyId___class_getitem__);
    if (tmp != NULL && PyFunction_Check(tmp)) {
        tmp = PyClassMethod_New(tmp);
        if (tmp == NULL)
            goto error;
        if (_PyDict_SetItemId(dict, &PyId___class_getitem__, tmp) < 0) {
            Py_DECREF(tmp);
            goto error;
        }
        Py_DECREF(tmp);
    }

    /* Add descriptors for custom slots from __slots__, or for __dict__ */
    ...
    ...
    //為class對象對應的instance對象設置內存大小信息    
    type->tp_basicsize = slotoffset;
    type->tp_itemsize = base->tp_itemsize;
    type->tp_members = PyHeapType_GET_MEMBERS(et);
	
    /* Initialize the rest */
    //調用PyType_Ready對class對象進行初始化
    if (PyType_Ready(type) < 0)
        goto error;

    /* Put the proper slots in place */
    fixup_slot_dispatchers(type);

    if (type->tp_dictoffset) {
        et->ht_cached_keys = _PyDict_NewKeysForClass();
    }

    if (set_names(type) < 0)
        goto error;

    if (init_subclass(type, kwds) < 0)
        goto error;

    Py_DECREF(dict);
    return (PyObject *)type;

error:
    Py_XDECREF(dict);
    Py_XDECREF(bases);
    Py_XDECREF(slots);
    Py_XDECREF(type);
    return NULL;
}

python虛擬機首先會將類名、基類列表和屬性表從tuple對象中解析出來,然后會基於基類列表及傳入的metaclass(參數metatype)確定最佳的metaclass和base,對於我們的A來說,最佳metaclass是type,最佳的base是object

隨后,python虛擬機會調用metatype->tp_alloc嘗試為要創建的類對象A分配內存。這里需要注意的是,在PyType_Type中,我們發現tp_alloc是一個NULL,這顯然不正常。但是不要忘記,我們之前提到,在python進行初始化時,會對所有的內建對象通過PyType_Ready進行初始化,在這個初始化過程中,有一項動作就是從基類繼承各種操作。由於type.__bases__中的第一基類object,所以type會繼承object中的tp_alloc操作,即PyType_GenericAlloc。對於我們的A(或者對於任何繼承自object的class對象)來說,PyType_GenericAlloc將申請metatype->tp_basicsize + metatype->tp_itemsize大小的內存空間。從PyType_Type的定義中我們看到,這個大小實際就是sizeof(PyHeapTypeObject) + sizeof(PyMemerDef)。因此在這里應該就明白了PyHeapTypeObject這個老鐵到底是干嘛用的了,之前因為偏移量的問題,折騰了不少功夫,甚至讓人覺得這有啥用啊,但是現在意識到了,這個老鐵是為用戶自定義class准備的。

此時,就是設置<class A>這個class對象的各個域,其中包括了在tp_dict上設置屬性表,也就是__dict__。另外注意的是,這里還計算了類對象A對應的實例對象所需要的內存大小信息,換言之,我們通過a = A()這樣的表達式創建一個instance對象時,需要為這個實例對象申請多大的內存空間呢?對於A(對任何繼承object的class對象也成立)來說,這個大小為PyBaseObject_Type->tp_basicsize + 16。其中的16是2 * sizeof(PyObject *)。為什么后面要跟着兩個PyObject *的空間,而且這些空間的地址被設置給了tp_dictoffset和tp_weaklistoffset了呢?這些留到以后解析。

最后,python虛擬機還會調用PyType_Ready對class A進行和內建對象一樣的初始化動作,到此A對應的class對象才算正式創建完畢。那么內建對象和class對象在內存布局上面有什么區別呢?畢竟都是類對象。

本質上,無論用戶自定義的class對象還是內建對象,在python虛擬機內部,都可以用一個PyTypeObject來表示。但不同的是,內建對象的PyTypeObject以及與其關聯的PyNumberMethods等屬性的內存位置都是在編譯時確定的,它們在內存中的位置是分離的。而用戶自定義的class對象的PyTypeObject和PyNumberMethods等內存位置是連續的,必須在運行時動態分配內存。

現在我們算是對python中可調用(callable)這個概念有一個感性任性了,在python中可調用這個概念是一個相當通用的概念,不拘泥於對象、大小,只要對象定義了tp_call操作,就能進行調用操作。我們已經看到,python中的對象class對象是調用metaclass創建。那么顯然,調用class對象就能得到實例對象。

13.4 從class對象到instance對象

上一章剖析函數機制的時候,真的是寫了我好長一段時間,當初剖析函數機制的時候,我就在想,函數都這么多,那類不得搞死我啊。事實上也確實如此,我現在也快寫煩了,這個類什么時候才能到頭啊,但即便如此也不要放棄,會柳暗花明的,也請你們堅持讀下去。

我們費了老鼻子勁創建了class對象,但僅僅是萬里長征的第一步。因為python虛擬機執行時,在內存中興風作浪的是一個個的實例對象,而class只是幕后英雄。

class A:

    name = "Python"

    def __init__(self):
        print("A->__init__")

    def f(self):
        print("A->self")

    def g(self, value):
        self.value = value
        print(self.value)


a = A()
a.f()
a.g(10)

我們只看模塊的字節碼,因為其它的已經看過了,就不貼了

  1           0 LOAD_BUILD_CLASS
              2 LOAD_CONST               0 (<code object A at 0x000001995CDA7C90, file "a.py", line 1>)
              4 LOAD_CONST               1 ('A')
              6 MAKE_FUNCTION            0
              8 LOAD_CONST               1 ('A')
             10 LOAD_NAME                0 (object)
             12 CALL_FUNCTION            3
             14 STORE_NAME               1 (A)

 16          16 LOAD_NAME                1 (A)
             18 CALL_FUNCTION            0
             20 STORE_NAME               2 (a)

 17          22 LOAD_NAME                2 (a)
             24 LOAD_METHOD              3 (f)
             26 CALL_METHOD              0
             28 POP_TOP

 18          30 LOAD_NAME                2 (a)
             32 LOAD_METHOD              4 (g)
             34 LOAD_CONST               2 (10)
             36 CALL_METHOD              1
             38 POP_TOP
             40 LOAD_CONST               3 (None)
             42 RETURN_VALUE

我們看到在將A build成類之后,通過14 STORE_NAME 1 (A)將剛剛創建的class對象存放到了globals(locals)命名空間中,並且符號也是A。然后16 LOAD_NAME 1 (A)重新將class對象取出壓入到運行時棧中,顯然從字節碼指令18 CALL_FUNCTION 0可以看出,是通過取出來的class對象來創建instance對象、也就是該類對象的實例對象,但是我們發現這個指令居然是CALL_FUNCTION,難道不是類嗎?其實最一開始看字節碼的時候我們就說了,即便是類和函數一樣,都是要先將PyCodeObject變成PyFunctionObject,然后對於類來說,再將PyFunctionObject對象通過LOAD_BUILD_CLASS指令build成一個類,因此在調用的時候,類和函數一樣,也是CALL_FUNCTION就很好理解了。python虛擬機在創建instance對象的時候,會通過指令20 STORE_NAME 2 (a)將(a, instance)組成一個entry存放在globals命名空間中,另外插一嘴,由於模塊級別的globals和locals命名空間是一個東西,但是對於函數來說globals和locals則不是一個東西,因此為了避免混淆,對於模塊來說,我只用globals命名空間,不用locals,盡管這兩者是一樣的,當用locals的時候,都會指函數或者類的locals。所以當這行代碼執行完畢之后,globals命名空間就會變成這樣

CALL_FUNCTION中,python同樣會執行對應類型的tp_call操作。所以創建實例的時候,顯然執行PyType_Type的tp_call,因此最終是在PyType_Type.tp_call中調用A.tp_new來創建instance對象的。

需要注意的是,在創建class A這個對象時,python虛擬機調用PyType_Ready對class A進行了初始化,其中一項動作就是集成基類,所以A.tp_new實際上就是object.tp_new,而在PyBaseObject_Type中,這個操作被定義為object_new。創建class對象和創建instance對象的不同之處正是在於tp_new不同。創建class對象,python虛擬機使用的是tp_new,創建instance,python虛擬機則使用object_new。使用類重寫__new__的話,應該很容易明白。

因此,由於我們創建的不是class對象,而是instance對象,type_call會嘗試進行初始化的動作

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    PyObject *obj;
    ...
    ...
    obj = type->tp_new(type, args, kwds);
    obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);
    if (obj == NULL)
        return NULL;

    /* Ugly exception: when the call was type(something),
       don't call tp_init on the result. */
    if (type == &PyType_Type &&
        PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
        (kwds == NULL ||
         (PyDict_Check(kwds) && PyDict_GET_SIZE(kwds) == 0)))
        return obj;

    /* If the returned object is not an instance of type,
       it won't be initialized. */
    if (!PyType_IsSubtype(Py_TYPE(obj), type))
        return obj;

    type = Py_TYPE(obj);
    if (type->tp_init != NULL) {
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            assert(PyErr_Occurred());
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!PyErr_Occurred());
        }
    }
    return obj;
}

對於基於class A創建的instance對象obj,其ob_type當然也在PyType_GenericAlloc中被設置為指向class A,其tp_init在PyType_Ready時會繼承PyBaseObject_Type的object_init操作。但正如我們之前說的那樣,因為A中的定義重寫了__init__,所以在fixup_slot_dispatchers中,tp_init會指向slotdef中指定的與__init__對應的slot_tp_init:

static int
slot_tp_init(PyObject *self, PyObject *args, PyObject *kwds)
{
    _Py_IDENTIFIER(__init__);
    int unbound;
    //虛擬機會通過lookup_method從class對象及其mro列表中搜索屬性__init__對應的操作
    PyObject *meth = lookup_method(self, &PyId___init__, &unbound);
    //返回結果
    PyObject *res;

    if (meth == NULL)
        return -1;
    if (unbound) {
        res = _PyObject_Call_Prepend(meth, self, args, kwds);
    }
    else {
        res = PyObject_Call(meth, args, kwds);
    }
    Py_DECREF(meth);
    if (res == NULL)
        return -1;
    //如果返回的不是None,那么報錯,這個信息熟悉不
    if (res != Py_None) {
        PyErr_Format(PyExc_TypeError,
                     "__init__() should return None, not '%.200s'",
                     Py_TYPE(res)->tp_name);
        Py_DECREF(res);
        return -1;
    }
    Py_DECREF(res);
    return 0;
}

所以如果你在定義class時,重寫了__init__操作,那么搜索的結果就是你寫的操作,如果沒有重寫那么執行object的__init__操作,而在object的__init__中,python虛擬機則什么也不做,而是直接返回。所以我們通過a = A()的方式來創建一個instance對象,實際上是沒有進行任何初始化的動作,因為我根本就沒有頭發,(⊙o⊙)…,因為A里面根本沒有__init__函數。

到了這里可以小結一下,從class對象創建instance對象的兩個步驟:

instance = class.__new__(class, *args, **kwargs)

class.__init__(instance, *args, **kwargs)

需要注意的是,這兩個步驟同樣也適用於從metaclass對象創建class對象,因為從metaclass對象創建class對象的過程其實和從class對象創建instance對象是一樣的,我們說class具有二象性。

13.5 訪問instance對象中的屬性

在前面的章節中我們討論命名空間時就提到,在python中,形如x.y形式的表達式稱之為"屬性引用",其中x為對象,y為對象的某個屬性,這個屬性可以是很多種,比如:整數、字符串、函數、類、甚至是模塊等等。在之前的a.py中,估計有人忘了,我們一共調用了兩個class A的成員函數,一個是不需要參數、一個是需要參數,當然有人會問不是還有__init__嗎?那個是在創建實例對象的時候自動調用的,我們就忽略了。

這是調用兩個成員函數的字節碼

 17          22 LOAD_NAME                2 (a)
             24 LOAD_METHOD              3 (f)
             26 CALL_METHOD              0
             28 POP_TOP

 18          30 LOAD_NAME                2 (a)
             32 LOAD_METHOD              4 (g)
             34 LOAD_CONST               2 (10)
             36 CALL_METHOD              1
             38 POP_TOP
             40 LOAD_CONST               3 (None)
             42 RETURN_VALUE

我們先來看看不需要參數的成員函數的調用過程是怎么樣的。

我們看到虛擬機首先通過LOAD_NAME將a對應的instance對象load進來、壓入運行時棧。接下來我們看到了一個LOAD_METHOD,是屬性、更精確的說應該是方法訪問機制的關鍵所在,它會從instance中獲取與符號f對應的對象,這個f是一個函數,所以這是一個PyFunctionObject對象。

//ceval.c
		TARGET(LOAD_METHOD) {
            /* Designed to work in tamdem with CALL_METHOD. */
            //這個name為PyUnicodeObject對象"f"
            PyObject *name = GETITEM(names, oparg);
            //而obj顯然是運行時棧中的那個instance對象
            PyObject *obj = TOP();
            //meth是一個PyObject *指針,顯然它要指向一個函數
            PyObject *meth = NULL;
			
            //這里是獲取方法,因此要獲取obj中符號的為name的函數,讓后讓meth指向它
            //所以meth是一個指針, 這里又傳入一個二級指針,然后讓meth存儲的地址變成指向對應方法(函數)的地址
            int meth_found = _PyObject_GetMethod(obj, name, &meth);
			
            //如果是NULL,說明沒有設置成功,在obj中找不到名為name的函數
            if (meth == NULL) {
                /* Most likely attribute wasn't found. */
                goto error;
            }
			
            //另外還返回了一個meth_found
            if (meth_found) {
                /* We can bypass temporary bound method object.
                   meth is unbound method and obj is self.

                   meth | self | arg1 | ... | argN
                 */
                //如果meth_found為1,說明meth是一個未綁定的方法,obj就是self
                //關於綁定和未綁定我們后面會詳細介紹
                //那么結果會變成這樣,相當於f(A, arg1, arg2...)
                SET_TOP(meth);
                PUSH(obj);  // self
            }
            else {
                /* meth is not an unbound method (but a regular attr, or
                   something was returned by a descriptor protocol).  Set
                   the second element of the stack to NULL, to signal
                   CALL_METHOD that it's not a method call.

                   NULL | meth | arg1 | ... | argN
                */
                //否則meth不是一個未綁定的方法,而是一個描述符協議返回的一個普通屬性、亦或是其他的什么東西
                //那么棧的第二個元素就會設置為NULL
                //因此結果就會變成這樣:f(None, arg1, arg2...)
                //其實造成這種局面無非是你傳錯實例,比如a = A()
                //那么a.f()就相當於A.f(a),但是你傳遞的不是a,而是其他的什么東西
                SET_TOP(NULL);
                Py_DECREF(obj);
                PUSH(meth);
            }
            DISPATCH();
        }

其實肯定有人想到了,獲取方法是LOAD_METHOD,那么獲取屬性呢?對,獲取屬性是LOAD_ATTR

//ceval.c
        TARGET(LOAD_ATTR) {
            //可以看到這個和LOAD_METHOD本質上是類似的,並且還要更簡單一些
            
            //name依舊是符號
            PyObject *name = GETITEM(names, oparg);
            //owner是所有者,為什么不叫obj,因為方法都是給實例用的,盡管類也能調用,但是方法畢竟是給實例用的
            //但是屬性的話,類和實例都可以訪問,各自互不干擾,所以是owner
            PyObject *owner = TOP();
            //res顯然就是獲取屬性返回的結果了
            PyObject *res = PyObject_GetAttr(owner, name);
            Py_DECREF(owner);
            //設置到棧頂
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }

LOAD_ATTRLOAD_METHOD這兩個指令集我們都看到了,但是里面具體實現的方法還沒有看,LOAD_ATTR調用了PyObject_GetAttr函數,LOAD_METHOD調用了_PyObject_GetMethod,我們來看看這兩個方法都長什么樣子。首先就從PyObject_GetAttr開始

//object.c
PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{	
    //*v:instance
    //*name:方法名
    PyTypeObject *tp = Py_TYPE(v);
	
    //如果傳遞的name不是一個PyUnicodeObject,直接報錯
    if (!PyUnicode_Check(name)) {
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return NULL;
    }
    //通過tp_getattro獲取屬性對應的對象
    if (tp->tp_getattro != NULL)
        return (*tp->tp_getattro)(v, name);
    //通過tp_getattr獲取屬性對應的對象
    if (tp->tp_getattr != NULL) {
        const char *name_str = PyUnicode_AsUTF8(name);
        if (name_str == NULL)
            return NULL;
        return (*tp->tp_getattr)(v, (char *)name_str);
    }
    //屬性不存在,拋出異常
    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%U'",
                 tp->tp_name, name);
    return NULL;
}

在python的class對象中,定義了兩個與屬性訪問相關的操作:tp_getattro和tp_getattr。其中tp_getattro是首先的屬性訪問動作,而tp_getattr在python中已不推薦使用。而這兩者的區別在PyObject_GetAttr中已經顯示的很清楚了,主要是在屬性名的使用上,tp_getattro所使用的屬性名必須是一個PyUnicodeObject對象,而tp_getattr所使用的屬性名是一個char *。因此如果某個類型定義了tp_getattro和tp_getattr,那么PyObject_GetAttr優先使用tp_getattro,因為這位老鐵寫在上面。

在python虛擬機創建class A時,會從PyBaseObject_Type中繼承其tp_getattro->PyObject_GenericGetAttr,所以python虛擬機會在這里進入PyObject_GenericGetAttr。不過由於涉及到了python中的描述符,所以看不懂沒關系,我們后面會詳細介紹描述符。

//object.c
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
    return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}

PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name,
                                 PyObject *dict, int suppress)
{
    /* Make sure the logic of _PyObject_GetMethod is in sync with
       this method.

       When suppress=1, this function suppress AttributeError.
    */
	//拿到obj的類型,對於我們的例子來說, 顯然是class A
    PyTypeObject *tp = Py_TYPE(obj);
    PyObject *descr = NULL;
    PyObject *res = NULL;
    descrgetfunc f;
    Py_ssize_t dictoffset;
    PyObject **dictptr;
	
    //name必須是str
    if (!PyUnicode_Check(name)){
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return NULL;
    }
    Py_INCREF(name);

    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            goto done;
    }
	
    
    //嘗試從mro列表中拿到f,等價於descr = A.f if hasattr(A, 'f') else NULL
    descr = _PyType_Lookup(tp, name);

    f = NULL;
    if (descr != NULL) {
        Py_INCREF(descr);
        //f = descr.__class__.__get__ 
        f = descr->ob_type->tp_descr_get;
        if (f != NULL && PyDescr_IsData(descr)) {
            //f不為NULL,並且是數據描述符,那么直接將描述符中__get__方法的結果返回
            //這個f就是描述里面的__get__方法,而這個descr就是描述符的一個實例對象
            //obj就是實例對象,(PyObject *)obj->ob_type是obj的類型
            res = f(descr, obj, (PyObject *)obj->ob_type);
            if (res == NULL && suppress &&
                    PyErr_ExceptionMatches(PyExc_AttributeError)) {
                PyErr_Clear();
            }
            goto done;
        }
    }
	
    //那么顯然要從instance對象自身的__dict__中尋找屬性
    if (dict == NULL) {
        /* Inline _PyObject_GetDictPtr */
        dictoffset = tp->tp_dictoffset;
        if (dictoffset != 0) {
            //但如果dict為NULL,並且dictoffset說明繼承自變長對象,那么要調整tp_dictoffset
            if (dictoffset < 0) {
                Py_ssize_t tsize;
                size_t size;

                tsize = ((PyVarObject *)obj)->ob_size;
                if (tsize < 0)
                    tsize = -tsize;
                size = _PyObject_VAR_SIZE(tp, tsize);
                assert(size <= PY_SSIZE_T_MAX);

                dictoffset += (Py_ssize_t)size;
                assert(dictoffset > 0);
                assert(dictoffset % SIZEOF_VOID_P == 0);
            }
            dictptr = (PyObject **) ((char *)obj + dictoffset);
            dict = *dictptr;
        }
    }
    //dict不為NULL,從字典中獲取
    if (dict != NULL) {
        Py_INCREF(dict);
        res = PyDict_GetItem(dict, name);
        if (res != NULL) {
            Py_INCREF(res);
            Py_DECREF(dict);
            goto done;
        }
        Py_DECREF(dict);
    }
	
    //我們看到這里又判斷了一次,但是這次少了個條件
    //沒錯熟悉python描述符的應該知道,上面的需要滿足是數據描述符
    //這個是非數據描述符
    if (f != NULL) {
        res = f(descr, obj, (PyObject *)Py_TYPE(obj));
        if (res == NULL && suppress &&
                PyErr_ExceptionMatches(PyExc_AttributeError)) {
            PyErr_Clear();
        }
        goto done;
    }
	
    //返回
    if (descr != NULL) {
        res = descr;
        descr = NULL;
        goto done;
    }
	
    //找不到,就報錯
    if (!suppress) {
        PyErr_Format(PyExc_AttributeError,
                     "'%.50s' object has no attribute '%U'",
                     tp->tp_name, name);
    }
  done:
    Py_XDECREF(descr);
    Py_DECREF(name);
    return res;
}

屬性訪問是從PyObject_GetAttr開始,那么下面我們來看看_PyObject_GetMethod

int
_PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method)
{
    PyTypeObject *tp = Py_TYPE(obj);
    PyObject *descr;
    descrgetfunc f = NULL;
    PyObject **dictptr, *dict;
    PyObject *attr;
    int meth_found = 0;

    assert(*method == NULL);

    if (Py_TYPE(obj)->tp_getattro != PyObject_GenericGetAttr
            || !PyUnicode_Check(name)) {
        *method = PyObject_GetAttr(obj, name);
        return 0;
    }

    if (tp->tp_dict == NULL && PyType_Ready(tp) < 0)
        return 0;

    descr = _PyType_Lookup(tp, name);
    if (descr != NULL) {
        Py_INCREF(descr);
        if (PyFunction_Check(descr) ||
                (Py_TYPE(descr) == &PyMethodDescr_Type)) {
            meth_found = 1;
        } else {
            f = descr->ob_type->tp_descr_get;
            if (f != NULL && PyDescr_IsData(descr)) {
                *method = f(descr, obj, (PyObject *)obj->ob_type);
                Py_DECREF(descr);
                return 0;
            }
        }
    }

    dictptr = _PyObject_GetDictPtr(obj);
    if (dictptr != NULL && (dict = *dictptr) != NULL) {
        Py_INCREF(dict);
        attr = PyDict_GetItem(dict, name);
        if (attr != NULL) {
            Py_INCREF(attr);
            *method = attr;
            Py_DECREF(dict);
            Py_XDECREF(descr);
            return 0;
        }
        Py_DECREF(dict);
    }

    if (meth_found) {
        *method = descr;
        return 1;
    }

    if (f != NULL) {
        *method = f(descr, obj, (PyObject *)Py_TYPE(obj));
        Py_DECREF(descr);
        return 0;
    }

    if (descr != NULL) {
        *method = descr;
        return 0;
    }

    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%U'",
                 tp->tp_name, name);
    return 0;
}

但是我們發現_PyObject_GetMethodPyObject_GetAttr基本是一致的,畢竟方法也可以看成是一種屬性嘛,這里就不介紹了。

13.5.1 instance對象中的_dict_

在屬性訪問的時候,我們可以通過a.__dict__這種形式訪問。但是這就奇怪了,在之前的描述中,我們看到從class A創建instance a的時候,python並沒有為instance創建PyDictObject對象。

但是在12.3.2 metaclass的時候,我們說過這樣一句話,對於A(對任何繼承object的class對象也成立)來說,這個大小為PyBaseObject_Type->tp_basicsize + 16。其中的16是2 * sizeof(PyObject *)。為什么后面要跟着兩個PyObject *的空間,而且這些空間的地址被設置給了tp_dictoffset和tp_weaklistoffset了呢?這些留到以后解析。。我們說多出來的兩個PyObject *留給了tp_dictoffset和tp_weaklistoffset,難道謎底出現在這里嗎?現在是時候揭開謎底了

在創建class A時,我們曾說,python虛擬機設置了一個名為tp_dictoffset的域,從名字推斷,這個可能就是instance對象中__dict__的偏移位置

虛線中畫出的dict對象就是我們期望中的a.__dict__,這個猜想可以在PyObject_GenericGetAttr中得到證實

//object.c
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
    return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}

PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name,
                                 PyObject *dict, int suppress)
{
    //那么顯然要從instance對象自身的__dict__中尋找屬性
    if (dict == NULL) {
        /* Inline _PyObject_GetDictPtr */
        dictoffset = tp->tp_dictoffset;
        if (dictoffset != 0) {
            //但如果dict為NULL,並且dictoffset說明繼承自變長對象,那么要調整tp_dictoffset
            if (dictoffset < 0) {
                Py_ssize_t tsize;
                size_t size;

                tsize = ((PyVarObject *)obj)->ob_size;
                if (tsize < 0)
                    tsize = -tsize;
                size = _PyObject_VAR_SIZE(tp, tsize);
                assert(size <= PY_SSIZE_T_MAX);

                dictoffset += (Py_ssize_t)size;
                assert(dictoffset > 0);
                assert(dictoffset % SIZEOF_VOID_P == 0);
            }
            dictptr = (PyObject **) ((char *)obj + dictoffset);
            dict = *dictptr;
        }
    }

如果dictoffset小於0,意味着A是繼承自類似str這樣的變長對象,python虛擬機會對dictoffset做一些處理,最終仍然會使dictoffset指向a的內存中額外申請的位置。而PyObject_GenericGetAttr正是根據這個dictoffset獲得了一個dict對象。更近一步,我們發現函數g中有設置self.value的代碼,這個instance對象的屬性設置也會訪問a.__dict__,而這個設置的動作最終會調用PyObject_GenericSetAttr,也就是a.__dict__最初被創建的地方。

//object.c
int
PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value)
{
    return _PyObject_GenericSetAttrWithDict(obj, name, value, NULL);
}

int
_PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name,
                                 PyObject *value, PyObject *dict)
{
    PyTypeObject *tp = Py_TYPE(obj);
    PyObject *descr;
    descrsetfunc f;
    PyObject **dictptr;
    int res = -1;
	
    //老規矩,name必須是PyUnicodeObject
    if (!PyUnicode_Check(name)){
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return -1;
    }

    if (tp->tp_dict == NULL && PyType_Ready(tp) < 0)
        return -1;

    Py_INCREF(name);
	
    //老規矩,獲取屬性
    descr = _PyType_Lookup(tp, name);
    if (descr != NULL) {
        Py_INCREF(descr);
        f = descr->ob_type->tp_descr_set;
        if (f != NULL) {
            res = f(descr, obj, value);
            goto done;
        }
    }

    if (dict == NULL) {
        //這行代碼就是PyObject_GenericGetAttr中根據dictoffset獲取dict對象的那段代碼
        dictptr = _PyObject_GetDictPtr(obj);
        if (dictptr == NULL) {
            if (descr == NULL) {
                PyErr_Format(PyExc_AttributeError,
                             "'%.100s' object has no attribute '%U'",
                             tp->tp_name, name);
            }
            else {
                PyErr_Format(PyExc_AttributeError,
                             "'%.50s' object attribute '%U' is read-only",
                             tp->tp_name, name);
            }
            goto done;
        }
        res = _PyObjectDict_SetItem(tp, dictptr, name, value);
    }
    else {
        Py_INCREF(dict);
        if (value == NULL)
            res = PyDict_DelItem(dict, name);
        else
            res = PyDict_SetItem(dict, name, value);
        Py_DECREF(dict);
    }
    if (res < 0 && PyErr_ExceptionMatches(PyExc_KeyError))
        PyErr_SetObject(PyExc_AttributeError, name);

  done:
    Py_XDECREF(descr);
    Py_DECREF(name);
    return res;
}

13.5.2 再論descriptor

前面我們看到,在PyType_Ready中,python虛擬機會填充tp_dict,其中與操作名對應的是一個個descriptor,那是我們看到的是descriptor這個概念在python內部是如何實現的。現在我們將要剖析的是descriptor在python的類機制中究竟會起到怎樣的作用。

在python虛擬機對class對象或instance對象進行屬性訪問時,descriptor將對屬性訪問的行為產生重大的影響。一般而言,對於一個對象obj,如果obj.__class__對應的class對象中存在__get__、__set__、__delete__操作(不要求三者同時存在),那么便可以稱之為描述符。在slotdefs中,我們會看到這三種魔法方法對應的操作。

//typeobject,c

    TPSLOT("__get__", tp_descr_get, slot_tp_descr_get, wrap_descr_get,
           "__get__($self, instance, owner, /)\n--\n\nReturn an attribute of instance, which is of type owner."),
    TPSLOT("__set__", tp_descr_set, slot_tp_descr_set, wrap_descr_set,
           "__set__($self, instance, value, /)\n--\n\nSet an attribute of instance to value."),
    TPSLOT("__delete__", tp_descr_set, slot_tp_descr_set,
           wrap_descr_delete,
           "__delete__($self, instance, /)\n--\n\nDelete an attribute of instance."),

前面我看到了PyWrapperDescrObject、PyMethodDescrObject等對象,它們對應的類對象中分別為tp_descr_get設置了wrapperdescr_get,method_get等函數,所以它們是當之無愧的descriptor。

另外如果細分,descriptor還可以分為兩種。(關於python中的描述符,我這里有一篇博客寫的很詳細,對描述符機制不太懂的話可以先去看看,https://www.cnblogs.com/traditional/p/11714356.html)

  • data descriptor:數據描述符,對應的__class__中定義了__get__和__set__的descriptor
  • no data descriptor:非數據描述符,對應的__class__中只定義了__get__方法

在python虛擬機訪問instance對象的屬性時,descriptor的一個作用就是影響python虛擬機對屬性的選擇。從PyObject_GenericGetAttr源碼中可以看到,python虛擬機會在instance對象自身的__dict__中尋找屬性,也會在instance對象對應的class對象的mro列表中尋找屬性,我們將前一種屬性稱之為instance屬性,后一種屬性稱之為class屬性。在屬性的選擇上,有如下規律。

python虛擬機優先按照instance屬性、class屬性的順序選擇屬性,即instance屬性優先於class屬性

如果在class屬性中發現同名的data descriptor,那么該descriptor會優先於instance屬性被python虛擬機選擇。

這兩條規則在對屬性進行設置時仍然會被嚴格遵守,換句話說,如果執行a.value = 1,而在A中出現了名為A的數據描述符,那么不好意思,會執行__set__方法,如果是非數據描述符,那么就不再走__set__了,而是設置屬性,相當於a.__dict__['value'] = 1

所以,獲取被描述符代理的屬性時,會直接調用__get__方法。設置的話,會調用__set__。當然要考慮優先級的問題,至於優先級的問題是什么,這里就不再解釋,強烈建立看我上面發的博客鏈接,對描述符的解析很詳細。

13.5.3 函數變身

在A的成員f的def語句中,我們分明看到一個self參數,self在python中是不是一個真正有效的參數呢?還是它僅僅只是一個語法意義是哪個的占位符而已?這一點可以從函數g中看到答案,在g中有這樣的語句:self.value = value,這條語句毫無疑問地揭示了self確實是一個實實在在的對象,所以表面上看起來f是一個不需要參數的函數,但實際上是一個貨真價值的帶參函數,只不過第一個參數自動幫你傳遞了,根據使用python的經驗我們知道,傳遞給self的就是實例本身。但是現在問題來了,這是怎么實現的呢?我們先再看一遍字節碼

 17          22 LOAD_NAME                2 (a)
             24 LOAD_METHOD              3 (f)
             26 CALL_METHOD              0
             28 POP_TOP

 18          30 LOAD_NAME                2 (a)
             32 LOAD_METHOD              4 (g)
             34 LOAD_CONST               2 (10)
             36 CALL_METHOD              1
             38 POP_TOP
             40 LOAD_CONST               3 (None)
             42 RETURN_VALUE

我們注意到,在LOAD完屬性a.f之后,會壓入運行時棧,然后直接就CALL_METHOD了,並且參數居然是個0。注意:這里是CALL_METHOD,不是CALL_FUNCTION。因此我們可以有兩條路可走,一條是看看CALL_METHOD是什么,另一條是再研究一下PyFunctionObject。我們先來看看CALL_METHOD這個指令長什么樣子吧

//ceval.c
        TARGET(CALL_METHOD) {
            /* Designed to work in tamdem with LOAD_METHOD. */
            PyObject **sp, *res, *meth;

            sp = stack_pointer;

            meth = PEEK(oparg + 2);
            if (meth == NULL) {
                res = call_function(&sp, oparg, NULL);
                stack_pointer = sp;
                (void)POP(); /* POP the NULL. */
            }
            else {

                res = call_function(&sp, oparg + 1, NULL);
                stack_pointer = sp;
            }

            PUSH(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }

//為了對比,我們再把CALL_FUNCTION的源碼貼出來
        TARGET(CALL_FUNCTION) {
            PyObject **sp, *res;
            sp = stack_pointer;
            res = call_function(&sp, oparg, NULL);
            stack_pointer = sp;
            PUSH(res);
            if (res == NULL) {
                goto error;
            }
            DISPATCH();
        }

通過對比,發現端倪,這兩個都調用了call_function,但是傳遞的參數不一樣,call_function的第二個參數一個oparg+1,一個是oparg,但是這還不足以支持我們找出問題所在。其實在剖析函數的時候,我們放過了PyFunctionObject的class ->PyFunction_Type。在這個PyFunction_Type中,隱藏着一個驚天大秘密。

PyTypeObject PyFunction_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "function",
    sizeof(PyFunctionObject),
    0,
    (destructor)func_dealloc,                   /* tp_dealloc */
    0,                                          /* tp_print */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_reserved */
    (reprfunc)func_repr,                        /* tp_repr */
    0,                                          /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    0,                                          /* tp_hash */
    function_call,                              /* tp_call */
    0,                                          /* tp_str */
    0,                                          /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,    /* tp_flags */
    func_new__doc__,                            /* tp_doc */
    (traverseproc)func_traverse,                /* tp_traverse */
    0,                                          /* tp_clear */
    0,                                          /* tp_richcompare */
    offsetof(PyFunctionObject, func_weakreflist), /* tp_weaklistoffset */
    0,                                          /* tp_iter */
    0,                                          /* tp_iternext */
    0,                                          /* tp_methods */
    func_memberlist,                            /* tp_members */
    func_getsetlist,                            /* tp_getset */
    0,                                          /* tp_base */
    0,                                          /* tp_dict */
    //注意注意注意注意注意注意注意,看下面這行
    func_descr_get,                             /* tp_descr_get */
    0,                                          /* tp_descr_set */
    offsetof(PyFunctionObject, func_dict),      /* tp_dictoffset */
    0,                                          /* tp_init */
    0,                                          /* tp_alloc */
    func_new,                                   /* tp_new */
};

我們發現與__get__相對應的tp_descr_get被設置成了func_descr_get,這意味着我們得到的是一個描述符,由於沒有設置tp_descr_set,所以A.f是一個非數據描述符。另外由於a.__dict__中沒有f,那么a.f的返回值將會被descriptor改變,其結果將是A.f.__get__,也就是func_descr_get(A.f, a, A)

//funcobject.c
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{
    if (obj == Py_None || obj == NULL) {
        Py_INCREF(func);
        return func;
    }
    return PyMethod_New(func, obj);
}

func_descr_get將A.f對應的PyFunctionObject進行了一番包裝,通過PyMethod_New,python虛擬機在PyFunctionObject的基礎上創建一個新的對象PyMethodObject,到PyMethod_New中一看,那個神秘的對象現身了:

//classobjet.c
PyObject *
PyMethod_New(PyObject *func, PyObject *self)
{
    PyMethodObject *im;
    if (self == NULL) {
        PyErr_BadInternalCall();
        return NULL;
    }
    im = free_list;
    if (im != NULL) {
        //使用緩沖池
        free_list = (PyMethodObject *)(im->im_self);
        (void)PyObject_INIT(im, &PyMethod_Type);
        numfree--;
    }
    else {
        //不適用緩沖池,直接創建PyMethodObject對象
        im = PyObject_GC_New(PyMethodObject, &PyMethod_Type);
        if (im == NULL)
            return NULL;
    }
    im->im_weakreflist = NULL;
    Py_INCREF(func);
    im->im_func = func;
    Py_XINCREF(self);
    //這里就是self
    im->im_self = self;
    _PyObject_GC_TRACK(im);
    return (PyObject *)im;
}

一切真相大白,原來那個神秘的對象就是PyMethodObject對象,看到free_list這樣熟悉的字眼,我們就知道python內部對PyMethodObject的實現和管理中使用緩沖池的技術。現在再來看看這個PyMethodObject

//classobject.h
typedef struct {
    PyObject_HEAD
    //可調用的PyFunctionObject對象
    PyObject *im_func;   /* The callable object implementing the method */
    //用於成員函數調用的self參數,instance對象
    PyObject *im_self;   /* The instance it is bound to */
    //弱引用列表
    PyObject *im_weakreflist; /* List of weak references */
} PyMethodObject;

在PyMethod_New中,分別將im_func,im_self設置了不同的值,對應的就是f對應PyFunctionObject對象、a對應的instance對象。因此通過PyMethodObject對象將PyFunctionObject對象和instance對象結合在一起(通過實例調用成員函數會先調用描述符、然后通過PyMethod_New將func和instance綁定起來得到PyMethodObject對象,調用函數相當於調用的是PyMethodObject,在調用PyMethodObject中會處理自動傳參的邏輯。就相當於在調用會自動將instance傳遞給第一個參數的PyFunctionObject,所以為什么實例調用方法的時候會自動傳遞第一個參數現在是真相大白了)這個過程稱之為成員函數的綁定,就是將實例和方法綁定起來,使之成為一個整體。

class A:

    def f(self):
        pass


a = A()
print(a.__class__.f)  # <function A.f at 0x00000190DD09FF70>
print(a.f)  # <bound method A.f of <__main__.A object at 0x00000190DD031F10>>

13.5.4 無參函數調用

 17          22 LOAD_NAME                2 (a)
             24 LOAD_METHOD              3 (f)
             26 CALL_METHOD              0
             28 POP_TOP

 18          30 LOAD_NAME                2 (a)
             32 LOAD_METHOD              4 (g)
             34 LOAD_CONST               2 (10)
             36 CALL_METHOD              1
             38 POP_TOP
             40 LOAD_CONST               3 (None)
             42 RETURN_VALUE

LOAD_METHOD指令結束之后,那么便開始了CALL_METHOD,我們知道這個和CALL_FUNCTION之間最大的區別就是,CALL_METHOD調用的是一個PyMethodObject對象,而CALL_FUNCTION調用的一個PyFunctionObject對象。

//ceval.c
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION
call_function(PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{
    PyObject **pfunc = (*pp_stack) - oparg - 1;
    PyObject *func = *pfunc;
    PyObject *x, *w;
    Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
    Py_ssize_t nargs = oparg - nkwargs;
    PyObject **stack = (*pp_stack) - nargs - nkwargs;

    ...
    ...    
    else {
        //從PyMethodObject對象中抽取PyFunctionObject和self參數
        if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
            /* Optimize access to bound methods. Reuse the Python stack
               to pass 'self' as the first argument, replace 'func'
               with 'self'. It avoids the creation of a new temporary tuple
               for arguments (to replace func with self) when the method uses
               FASTCALL. */
            //記住這個宏,從名字也能看出來這是獲取self的
            PyObject *self = PyMethod_GET_SELF(func);
            Py_INCREF(self);
            func = PyMethod_GET_FUNCTION(func);
            Py_INCREF(func);
            //self參數入棧,調整參數信息變量
            Py_SETREF(*pfunc, self);
            //參數個數加一
            nargs++;
            stack--;
        }
        else {
            Py_INCREF(func);
        }
		
        //還記得這個嗎?一個是快速通道,一個不是快速通道
        //之前我們介紹調用的時候,有時直接就跳到這里了_PyObject_FastCallKeywords
        //但是這個函數是通過call_function來調用的。
        if (PyFunction_Check(func)) {
            x = _PyFunction_FastCallKeywords(func, stack, nargs, kwnames);
        }
        else {
            x = _PyObject_FastCallKeywords(func, stack, nargs, kwnames);
        }
        Py_DECREF(func);
    }
    ...
    ...    
    return x;
}

顯然在調用成員函數f的時候,顯式傳入的參數個數為0,也就是說調用f的時候,python虛擬機沒有進行參數入棧的動作。而f顯然又是一個需要參數的函數,其參數為self。因此在call_function中,python會為PyMethodObject對象進行了一些參數處理的動作。

當python虛擬機執行a.f()的時候,在call_function中,會執行PyMethod_GET_SELF,因為條件顯然是成立的。

#define PyMethod_GET_FUNCTION(meth) \
        (((PyMethodObject *)meth) -> im_func)
#define PyMethod_GET_SELF(meth) \
        (((PyMethodObject *)meth) -> im_self)

在call_function中,func變量指向一個PyMethodObject對象,然后會將PyFunctionObject對象和instance對象分別提取出來。

		   PyObject *self = PyMethod_GET_SELF(func);
            ...   
            //self參數入棧,調整參數信息變量
            Py_SETREF(*pfunc, self);

在分析函數機制的時候,我們也看到了這個pfunc,它指向的位置正是存放PyMethodObject對象的位置。那么在這個本來屬於PyMethodObject的地方存放instance對象究竟有什么作用呢。其實python虛擬機以另一種方式完成了函數參數入棧的動作,而這個本來屬於PyMethodObject對象的內存空間現在被用作了函數f的self參數的容身之處。我們先來看看調用call_function后運行時棧的變化情況:

我們觀察設置pfunc之前和之后的運行時棧,貌似沒什么區別,我們往后看。

在call_function,接着還會通過PyMethod_GET_FUNCTION將PyMethodObject對象中的PyFunctionObject提取出來,隨后完成了self的入棧,同時還調整了維護這參數信息的nargs和stack。回憶之前的隊函數機制的分析,我們可以發現,調整的函數會獲得第一個位置參數,沒錯,就是self

由於func在if分支之后指向了PyFunctionObject對象,所以接下來python虛擬機將會執行_PyFunction_FastCallKeywords或者_PyObject_FastCallKeywords。到了這里,我們已經可以勢如破竹了,因為剩下的動作和我們之前分析的帶參函數的調用一致,而a.f實際上就是個帶一個位置參數的一般函數的調用。而在_PyFunction_FastCallKeywords中,作為self參數的instance被python虛擬機壓入到了運行時棧中。通過之前對函數的分析,我們知道,由於a.f僅僅是個帶一個位置參數的函數,所以將會走快速通道,因此會調用_PyFunction_FastCallKeywords,在快速通道中,運行時棧中的這個instance對象會被拷貝到新的PyFrameObject對象的f_localsplus中。還記得快速通道的邏輯嗎?

//call.c
PyObject *
_PyFunction_FastCallKeywords(PyObject *func, PyObject *const *stack,
                             Py_ssize_t nargs, PyObject *kwnames)
{	
    PyObject **stack = (*pp_stack) - nargs - nkwargs;
    ...
    ...
    if (co->co_kwonlyargcount == 0 && nkwargs == 0 &&
        (co->co_flags & ~PyCF_MASK) == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
    {
        if (argdefs == NULL && co->co_argcount == nargs) {
            //調用function_code_fastcall
            //參數stack就是運行時棧
            return function_code_fastcall(co, stack, nargs, globals);
        }
        else if (nargs == 0 && argdefs != NULL
                 && co->co_argcount == PyTuple_GET_SIZE(argdefs)) {
            stack = &PyTuple_GET_ITEM(argdefs, 0);
            return function_code_fastcall(co, stack, PyTuple_GET_SIZE(argdefs),
                                          globals);
        }
    }
	
    return _PyEval_EvalCodeWithName((PyObject*)co, globals, (PyObject *)NULL,
                                    stack, nargs,
                                    nkwargs ? &PyTuple_GET_ITEM(kwnames, 0) : NULL,
                                    stack + nargs,
                                    nkwargs, 1,
                                    d, (int)nd, kwdefs,
                                    closure, name, qualname);
}



static PyObject* _Py_HOT_FUNCTION
function_code_fastcall(PyCodeObject *co, PyObject *const *args, Py_ssize_t nargs,
                       PyObject *globals)
{
    PyFrameObject *f;
    PyThreadState *tstate = PyThreadState_GET();
    PyObject **fastlocals;
    Py_ssize_t i;
    PyObject *result;

    assert(globals != NULL);
    /* XXX Perhaps we should create a specialized
       _PyFrame_New_NoTrack() that doesn't take locals, but does
       take builtins without sanity checking them.
       */
    assert(tstate != NULL);
    //創建新的PyFrameObject對象f,tstate是當前線程,后續章節會介紹,包括python的多線程,GIL等等
    f = _PyFrame_New_NoTrack(tstate, co, globals, NULL);
    if (f == NULL) {
        return NULL;
    }
	
    //拿到f_localsplut
    fastlocals = f->f_localsplus;
	
    //這里的nargs就是上面傳遞的stack
    for (i = 0; i < nargs; i++) {
        Py_INCREF(*args);
        //拷貝self參數到f_localsplus中
        fastlocals[i] = *args++;
    }
    result = PyEval_EvalFrameEx(f,0);

    if (Py_REFCNT(f) > 1) {
        Py_DECREF(f);
        _PyObject_GC_TRACK(f);
    }
    else {
        ++tstate->recursion_depth;
        Py_DECREF(f);
        --tstate->recursion_depth;
    }
    return result;
}

在調用_PyFunction_FastCallKeywords之后,參數的數量已經從0變成了1,所以stack指向的位置就和pfunc指向的位置是一致的了。然后為了要將instance作為參數拷貝到函數的參數區fastlocals中,就必須要將它放到棧頂,也就是PyMethodObject對象所在的位置上,這就是前面要執行Py_SETREF(*pfunc, self);將self入棧的原因。

13.5.5 帶參函數的調用

分析了類中無參的成員函數的調用,那么帶參函數也就水到渠成了,因為流程是一致的,我們看看帶參函數的字節碼。

 17          22 LOAD_NAME                2 (a)
             24 LOAD_METHOD              3 (f)
             26 CALL_METHOD              0
             28 POP_TOP

 18          30 LOAD_NAME                2 (a)
             32 LOAD_METHOD              4 (g)
             34 LOAD_CONST               2 (10)
             36 CALL_METHOD              1
             38 POP_TOP
             40 LOAD_CONST               3 (None)
             42 RETURN_VALUE

無視POP_TOP及其下面的指令,我們和無參函數對比下,看看有什么不同。其實就是多了個LOAD_CONST,而這個指令顯然不陌生了,就是將一個常量壓入的運行時棧中。如果多個參數,就將多個參數壓入到運行時棧,這就是無參成員函數和帶參成員函數的唯一不同之處,想想無參函數的調用流程,帶參函數的調用流程也就迎刃而解了。說白了,不就是將self和我們傳入的參數一起壓入到運行時棧中嘛。

因此到了這里,我們可以在更高層次俯視一下python的運行模型了,最核心的模型非常簡單,可以簡化為兩條規則:

在某個命名空間中尋找符號對應的對象

對從命名空間中得到的對象進行某些操作

拋開面向對象這些花里胡哨的外表,其實我們發現class對象其實就是一個命名空間,instance對象也是一個命名空間,不過這些命名空間通過一些特殊的規則連接在一起,使得符號的搜索過程變得復雜,從而實現了面向對象這種編程模式,それだけ

13.5.6 bound method和unbound method

在python中,當對作為方法(或者說作為屬性的函數)進行引用時,會有兩種形式,bound method和unbound method

bound method:這種形式是通過實例對象進行屬性引用,就像我們之前說的a.f這樣

unbound method:這種形式是通過類對象進行屬性引用,比如A.f

當然這兩種引用方式是不同的,我們可以看一下字節碼指令

class A(object):

    def f(self):
        pass
    

a = A()
A.f(a)
a.f()
  8          22 LOAD_NAME                1 (A)
             24 LOAD_METHOD              3 (f)
             26 LOAD_NAME                2 (a)
             28 CALL_METHOD              1
             30 POP_TOP

  9          32 LOAD_NAME                2 (a)
             34 LOAD_METHOD              3 (f)
             36 CALL_METHOD              0
             38 POP_TOP
             40 LOAD_CONST               2 (None)
             42 RETURN_VALUE

光看字節碼好像看不出來什么,還要從LOAD_METHOD指令最終調用type_getattro。在type_getattro中,會在class A的tp_dict中發現f對應的PyFunctionObject,同樣因為它是一個descriptor,一次你也會調用其__get__函數進行轉變。在之前剖析a.f時,我們看到這個轉變是通過func_descr_get(A.f, a, A)完成的,但前提是實例調用。如果是這里的類調用,那么不好意思,就會變成這樣,func_descr_get(A.f, NULL, A)。因此雖然A.f調用得到的也是一個PyMethodObject,但其中的im_self確實NULL。

在python中,bound method和unbound method的本質區別就在於PyFunctionObject有沒有與instance對象綁定在PyMethodObject對象。bound method完成了綁定動作,而unbound method沒有完成綁定動作。

所以在對unbound method進行調用時,我們必須要顯示的傳遞一個instance對象(哪怕這個instance對象不是當前class對象的實例也可以,具體什么意思后面會演示)作為成員函數的第一個參數,因為f無論如何都需要一個self參數,所有才會有A.f(a)這種形式。而無論是對unbound method進行調用,還是對bound method進行調用,python虛擬機的動作本質都是一樣的,都是調用帶位置參數的一般函數。區別只在於:當調用bound method時,python虛擬機幫我們完成了PyFunctionObject對象和instance對象的綁定,instance對象將自動成為self參數;而調用unbound method時,沒有這個綁定,我們需要自己傳入self參數。

class A(object):

    def f(self):
        print(self)


a = A()
A.f(123)  # 123
# 我們看到即便傳入一個123,不傳入a也是可以的
# 這是我們自己傳遞的,傳遞什么就是什么

a.f()  # <__main__.A object at 0x000001F0FFE81F10>
# 但是a.f()就不一樣了,調用的時候會自動將a作為參數傳遞給self

print(A.f)  # <function A.f at 0x000001F0FFEEFF70>
print(a.f)  # <bound method A.f of <__main__.A object at 0x000001F0FFE81F10>>

13.6 千變萬化的descriptor

當我們調用instance對象的函數時,最關鍵的一個動作就是從PyFunctionObject對象向PyMethodObject對象的轉變,而這個關鍵的轉變就取決於python中的descriptor。當我們訪問對象中的屬性時,由於descriptor的存在,這種轉換自然而然的就發生了。事實上,python中的descriptor很強大,我們可以使用它做很多事情,而在python的內部,也存在各種各樣的descriptor,比如property、staticmethod、classmethod等等,這些descriptor給python的類機制賦予了強大的力量。具體源碼就不分析了,因為本質上是一樣的,staticmethod就是一個類,然后調用的時候,會觸發這個類的__get__方法,我們直接通過python代碼的層面演示一下,這三種描述符的實現。

實現property

class Property:

    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        if instance is None:
            # 如果instance是None說明是實例調用,那么直接返回這個描述符本身
            # 這個和內置property的處理方式是一樣
            return self
        res = self.func(instance)
        return res


class A:

    @Property
    def f(self):
        return "name: hanser"


a = A()
print(a.f)  # name: hanser
print(A.f)  # <__main__.Property object at 0x000001FABFE910A0>

實現staticmethod

class StaticMethod:

    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        # 靜態方法的話,類和實例都可以用
        # 類調用不會自動傳參,但是實例會自動傳遞,因此我們需要把實例調用傳來的self給扔掉

        # 做法是直接返回self.func即可,注意:self.func是A.func
        # 因此調用的時候,是類去調用的,而類調用是不會自動加上參數的。
        return self.func


class A:

    @StaticMethod
    def f():
        return "name: hanser"


a = A()
print(a.f())  # name: hanser
print(A.f())  # name: hanser

實現classmethod

class ClassMethod:

    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        # 類方法,目的是在類調用的時候,將類本身作為第一個參數傳遞進去
        # 顯然是這里的owner

        # 返回一個閉包,然后當調用的時候,接收參數
        # 不管是誰調用,最終這個self.func都是A.func,然后手動將cls也就是owner傳遞進去
        def inner(*args, **kwargs):
            return self.func(owner, *args, **kwargs)
        return inner


class A:

    name = "hanser"

    @ClassMethod
    def f(cls):
        return f"name: {cls.name}"


a = A()
print(a.f())  # name: hanser
print(A.f())  # name: hanser

13.7 おしまい

以上就是python關於類的剖析,到此就結束啦。累死我了。。。。。


免責聲明!

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



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