《深度剖析CPython解釋器》3. Python的引用計數是什么?Python底層是如何管理對象的?


楔子

在上一篇中我們說到了Python中的對象在底層的數據結構,我們知道Python底層通過PyObject和PyTypeObject完成了C++所提供的對象的多態特性。在Python中創建一個對象,會分配內存並進行初始化,然后Python會用一個PyObject *來保存和維護這個對象,當然所有對象都是如此。因為指針是可以相互轉化的,所以變量在保存一個對象的指針時,會將該指針轉成PyObject *之后再交給變量保存。因此在Python中,變量的傳遞(包括函數的參數傳遞)實際上傳遞的都是一個泛型指針:PyObject *。這個指針具體是指向的什么類型我們並不知道,只能通過其內部的ob_type成員進行動態判斷,而正是因為這個ob_type,Python實現了多態機制。

比如:a.pop(),我們不知道這個a指向的對象到底是什么類型,但只要a可以調用pop方法即可,因此a可以是一個列表、也可以是一個字典、或者是我們實現了pop方法的類的實例對象。所以如果a的ob_type是一個PyList_Type *,那么就調用PyList_Type中定義的pop操作;如果a的ob_type是一個PyDict_Type,那么就調用PyDict_Type中定義的pop操作。

所以變量a在不同的情況下,會表現出不同的行為,這正是Python多態的核心所在。

再比如列表,其內部的元素都是PyObject *,當我們通過索引獲取到該指針進行操作的時候,會先通過ob_type獲取其類型指針,然后再獲取該操作對應的C一級的函數、進行執行,如果不支持相應的操作便會報錯。

從這里我們也能看出來Python為什么慢了,因為有相當一部分時間浪費在類型和屬性的查找上面。

以變量a + b為例,這個a和b指向的對象可以是整型、浮點型、字符串、列表、元組、甚至是我們自己實現了某個魔法方法的類的實例對象,因為我們說Python中的變量都是一個PyObject *,所以它可以指向任意的對象,因此Python它就無法做基於類型方面的優化。

首先Python要通過ob_type判斷變量到底指向的是什么類型,這在C級至少需要一次屬性查找。然后Python將每一個操作都抽象成了一個魔法方法,所以實例相加時要在對應的類型對象中找到該方法對應的函數指針,這又是一次屬性查找。找到了之后將a、b作為參數傳遞進去,這會發生一次函數調用,會將a和b中維護的值拿出來進行運算,然后根據相加結果創建一個新的對象,再返回其對應的PyObject *指針。

而對於C來講,由於已經規定好了類型,所以a + b在編譯之后就是一條簡單的機器指令,所以兩者在效率上差別很大。

當然我們不是來吐槽Python效率的問題的,因為任何語言都擅長的一面和不擅長的一面,只是通過回顧前面的知識來解釋為什么Python效率慢。

因此當別人問你Python為什么效率低的時候,希望你能從這個角度來回答它。不要動不動就GIL,那是在多線程情況下才需要考慮的問題,所以有時真的很反感那些在沒涉及到多線程的時候還提Python GIL的人。

簡單回顧了一下前面的內容,下面我們說一說Python中的對象從創建到銷毀的過程,了解一下Python中對象的生命周期。

Python/C API

當我們在控制台敲下這個語句的時候,Python內部是如何從無到有創建一個浮點數對象的?

>>> e = 2.71

另外Python又是怎么知道該如何將它打印到屏幕上面呢?

>>> print(e)
2.71

對象使用完畢時,Python還要將其銷毀,那么銷毀的時機又該如何確定呢?帶着這些問題,我們來探尋一個對象從創建到銷毀整個生命周期中的行為表現,然后從中尋找答案。

不過在探尋對象的創建之前,先介紹Python提供的C API,也叫Python/C API。

Python對外提供了C API,讓用戶可以從C環境中與其交互。實際上,由於Python解釋器是用C寫成的,所以Python內部本身也在大量使用這些C API。為了更好的研讀源碼,系統地了解這些API的組成結構是很有必要的,而C API分為兩類:"泛型API""特型API"

泛型API

"泛型API"與類型無關,屬於"抽象對象層(Abstract Object Layer,AOL)",這類API的第一個參數是PyObject *,可以處理任意類型的對象,API內部會根據對象的類型進行區別處理。而且泛型API名稱也是有規律的,具有PyObject_xxx這種形式。

以對象打印函數為例:

int PyObject_Print(PyObject *op, FILE *fp, int flags)

接口的第一個參數為待打印的對象的指針,可以是任意類型的對象的指針,因此參數類型是PyObject *。而我們說PyObject *是Python底層的一個泛型指針,通過這個泛型指針來實現多態的機制。第二個參數是文件句柄,表示輸出的位置,默認是stdout、即控制台;而flags表示是要以__str__打印還是要以__repr__打印。

// 假設有兩個PyObject *, fo和lo
// fo指向PyFloatObject, lo指向PyLongObject, 但是它們在打印的時候都可以調用這個相同的打印方法
PyObject_Print(fo, stdout, 0);
PyObject_Print(lo, stdout, 0);

PyObject_Print接口內部會根據對象類型,決定如何輸出對象。

特型API

特型API與類型相關,屬於"具體對象層(Concrete Object Layer,COL)"。這類API只能作用於某種具體類型的對象,比如:浮點數PyFloatObject,而Python內部為每一種內置對象的實例對象都提供了很多的特型API。比如:

// 通過C的中double創建PyFloatObject
PyObject* PyFloat_FromDouble(double v);

// 通過C中的long創建PyLongObject
PyObject* PyLong_FromLong(long v);
// 通過C中的char *來創建PyLongObject
PyObject* PyLong_FromString(const char *str, char **pend, int base)

特型API也是有規律的,尤其是關於C類型和Python類型互轉的時候,會用到以下兩種特型API:

  • Py###_From@@@: 根據C的對象創建Python的對象,###表示Python的類型, @@@表示C的類型,比如PyFloat_FromDouble表示根據C中的double創建Python的float。
  • Py###_As@@@: 根據Python的對象創建C的對象, ###表示Python的類型,@@@表示C的類型,比如PyFloat_AsDouble表示根據Python的float創建C的double; PyLong_AsLong表示根據Python中的int創建C中的long,因為Python中的int是沒有長度限制的,所以在底層使用的是PyLongObject,而不是PyIntObject。

了解了Python/C API之后,我們看對象是如何創建的。

對象的創建

經過前面的理論學習,我們知道對象的元數據保存在對應的類型對象,元數據當然也包括對象如何創建等信息。

比如執行pi = 3.14,那么這個過程都發生了什么呢?首先解釋器會根據3.14推斷出要創建的對象是浮點數,所以會創建出維護的值為3.14的PyFloatObject,並將其指針轉化成PyObject *交給變量pi。

另外需要注意的是,我們說對象的元數據保存在對應的類型對象中,這就意味着對象想要被創建是需要借助對應的類型對象的,但是這是針對於創建我們自定義的類的實例對象而言。創建內置類型的實例對象是直接創建的,至於為什么,我們下面會說。

而創建對象的方式有兩種,一種是通過"泛型API"創建,另一種是通過"特型API"創建。比如創建一個浮點數:

使用泛型API創建:

PyObject* pi = PyObject_New(PyObject, &PyFloat_Type);

使用特型API創建:

PyObject* pi = PyFloat_FromDouble(3.14);

//創建一個內部可以容納5個元素的PyTupleObject
PyObject* tpl = PyTuple_New(5);
//創建一個內部可以容納5個元素的PyListObject, 當然了這是初始容量, 列表可以擴容的
PyObject* tpl = PyList_New(5);    

但不管采用哪種方式創建,最終的關鍵步驟都是分配內存,而創建內置類型的實例對象,Python是可以直接分配內存的。因為它們有哪些成員在底層都是寫死的,而Python對它們了如指掌,因此可以通過Python/C API直接分配內存並初始化。以PyFloat_FromDouble為例,直接在接口內部為PyFloatObject結構體實例分配內存,並初始化相關字段即可。

比如:pi = 3.14,解釋器通過3.14知道要創建的對象是PyFloatObject,那么直接根據PyFloatObject里面的成員算一下就可以了,一個引用計數(ob_refcnt) + 一個指針(ob_type) + 一個double(ob_fval) 顯然是24個字節,所以直接就分配了。然后將ob_refcnt始化為1,ob_type設置為&PyFloat_Type,ob_fval設置為3.14即可。

同理可變對象也是一樣,因為成員都是固定的,類型、以及內部容納的元素有多少個也可以根據賦的值得到,所以內部的所有元素(PyObject *)占用了多少內存也是可以算出來的,因此也是可以直接分配內存的。

但對於我們自定義的類型就不行了,假設我們通過class Girl:定義了一個類,顯然實例化的時候不可能通過PyGirl_New、或者PyObject_New(PyObject, &PyGirl_Type)這樣的API去創建,因為根本就沒有PyGirl_New這樣的API,也沒有PyGirl_Type這個類型對象。這種情況下,創建Girl的實例對象就需要Girl這個類型對象來創建了。因此自定義類的實例對象如何分配內存、如何進行初始化,答案是需要在對應的類型對象里面尋找的。

總的來說:Python內部創建一個對象的方法有兩種:

  • 通過Python/C API,可以是泛型API、也可以是特型API,用於內置類型;
  • 通過對應的類型對象去創建,多用於自定義類型;

拋出個問題: e = 2.71 和 e = float(2.71)得到的結果都是2.71,但它們之間有什么不同呢。或者說列表: lst = [] 和 lst = list()得到的lst也都是一個空列表,但這兩種方式有什么區別呢?

我們說創建實例對象可以通過Python/C API,用於內置類型;也可以通過對應的類型對象去創建,多用於自定義類型。但是通過對應類型對象去創建實例對象其實是一個更加通用的流程,因為它除了支持自定義類型之外、還支持內置類型。比如:

>>> lst = []  # 通過Python/C API創建
>>> lst
[]
>>> lst = list()  # 通過類型對象創建
>>> lst
[]
>>> e = 2.71  # 通過Python/C API創建 
>>> e
2.71
>>> e = float(2.71)  # 通過類型對象創建
>>> e
2.71
>>>

所以我們看到了對象的兩種創建方式,我們寫上2.71、或者[],Python會直接解析成底層對應的數據結構;而float(2.71)、或者list(),雖然結果是一樣的,但是我們看到這是一個調用,因此要進行參數解析、類型檢測、創建棧幀、銷毀棧幀等等,所以開銷會大一些。

import time

t1 = time.perf_counter()
for _ in range(10000000):
    lst = []
t2 = time.perf_counter()
print(t2 - t1)  # 0.5595989


t3 = time.perf_counter()
for _ in range(10000000):
    lst = list()
t4 = time.perf_counter()
print(t4 - t3)  # 1.1722419999999998

通過[]的方式創建一千萬次空列表需要0.56秒,但是通過list()的方式創建一千萬次空列表需要1.17秒,主要就在於list()是一個調用,而[]直接會被解析成底層對應的PyListObject,因此[]的速度會更快一些。同理3.14和float(3.14)也是如此。

雖說使用Python/C API的方式創建的速度會更快一些,但這是針對內置類型而言。以我們上面那個自定義了Girl為例,如果想創建一個Girl的實例對象,除了通過Girl這個類型對象去創建,你還能想到其它方式嗎?

列表的話:可以list()、也可以[];元組:可以tuple()、也可以();字典:可以dict()、也可以{},前者是通過類型對象去創建的,后者是通過Python/C API創建,會直接解析為對應的C一級數據結構。因為這些結構在底層都是已經實現好了的,是可以直接用的,無需通過調用的方式。

但是顯然自定義類型就沒有這個待遇了,它的實例對象只能通過它自己去創建,比如:Girl這個類,Python不可能在底層定義一個PyGirlObject、然后把API提供給我們。所以,我們只能通過Girl()這種方式去創建Girl的實例對象。

所以我們需要通過Girl這個類來創建它的實例對象,也就是調用Girl這個類,而一個對象可以是可調用的,也可以是不可調用的。如果一個對象可以被調用,那么這個對象就是callable,否則就不是callable。

而決定一個對象是不是callable,就取決於其對應的類型對象中是否定義了某個方法。如果從Python的角度看的話,這個方法就是__call__,從解釋器角度看的話,這個方法就是tp_call。

1. 從Python的角度來看對象的調用:

# int可以調用, 那么它的類型對象(type)內部一定有__call__方法
print(hasattr(type, "__call__"))  # True


class A:
    pass


a = A()
# 因為我們自定義的類A里面沒有__call__, 所以a是不可以被調用的
try:
    a()
except Exception as e:
    # 告訴我們A的實例對象不可以被調用
    print(e)  # 'A' object is not callable


# 如果我們給A設置了一個__call__
type.__setattr__(A, "__call__", lambda self: "這是__call__")
# 發現可以調用了
print(a())  # 這是__call__

# 我們看到這就是動態語言的特性, 即便在類創建完畢之后, 依舊可以通過type進行動態設置
# 而這在靜態語言中是不支持的, 所以type是所有類的元類, 它控制了我們自定義類的生成過程
# type這個古老而又強大的類可以讓我們玩出很多新花樣
# 但是對於內置的類type是不可以對其動態增加、刪除或者修改的,因為內置的類在底層是靜態定義好的
# 因為從源碼中我們看到, 這些內置的類、包括元類,它們都是PyTypeObject對象, 在底層已經被聲明為全局變量了
# 所以type雖然是所有類型對象的元類,但是只有在面對我們自定義的類的時候,type具有增刪改的能力

try:
    type.__setattr__(dict, "__call__", lambda self: "這是__call__")
except Exception as e:
    print(e)  # can't set attributes of built-in/extension type 'dict'
# 我們看到拋異常了, 提示我們"不可以給內置/擴展類型dict設置屬性"
# 而dict屬於內置類型,至於擴展類型是我們在編寫擴展模塊中定義的類
# 內置類和擴展類是等價的,它們直接就指向了C一級的數據結構, 不需要經歷被解釋器解釋這一步
# 而動態特性是解釋器在解釋執行字節碼(翻譯成C級代碼執行)的時候動態賦予的
# 而內置類/擴展類它們本身就已經是指向C一級的數據結構了,繞過了解釋器解釋執行這一步, 所以它們的屬性不能被動態設置


try:
    int.__dict__["a"] = "b"
except Exception as e:
    # 它們的屬性字典也是不可以設置的
    print(e)  # 'mappingproxy' object does not support item assignment


class Girl: pass

g = Girl()
g.a = "xx"
# 實例對象我們也可以手動設置屬性
print(g.a)  # xx


lst = list()
try:
    lst.a = "xx"
except Exception as e:
    # 但是內置類型的實例對象是不可以的
    print(e)  # 'list' object has no attribute 'a'
# 可能有人奇怪了,為什么不行呢?
# 答案是內置類型的實例對象沒有__dict__屬性字典, 有多少屬性或方法底層已經定義好了,不可以動態添加
# 如果我們自定義類的時候,設置了__slots__, 那么效果和內置的類是相同的
print(hasattr(lst, "__dict__"))  # False

2. 從解釋器的角度來看對象的調用:

我們以內置類型float為例,我們說創建一個PyFloatObject,可以通過3.14或者float(3.14)的方式。前者使用Python/C API創建,3.14直接被解析為C一級數據結構PyFloatObject的對象;后者使用類型對象創建,通過對float進行一個調用、將3.14作為參數,最終也得到指向C一級數據結構PyFloatObject的對象。Python/C API的創建方式我們已經很清晰了,就是根據值來推斷在底層應該對應哪一種數據結構,然后直接創建即可。我們重點看一下通過調用來創建實例對象的方式。

如果一個對象可以被調用,我們說它的類型對象中一定要有tp_call(更准確的說成員tp_call的值一定一個是函數指針, 不可以是0),而PyFloat_Type是可以調用的,這就說明PyType_Type內部的tp_call是一個函數指針,這在Python的層面是上我們已經驗證過了,下面我們就來看看。

//typeobject.c
PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type",                                     /* tp_name */
    sizeof(PyHeapTypeObject),                   /* tp_basicsize */
    sizeof(PyMemberDef),                        /* tp_itemsize */
    (destructor)type_dealloc,                   /* tp_dealloc */
    //...                                          /* tp_hash */
    (ternaryfunc)type_call,                     /* tp_call */
    //...
}        

我們看到在實例化PyType_Type的時候PyTypeObject內部的成員tp_call被設置成了type_call,這是一個函數指針,當我們調用PyFloat_Type的時候,會觸發這個type_call指向的函數。

因此float(3.14)在C的層面上等價於:

PyFloat_Type.ob_type.tp_call(&PyFloat_Type, args, kwargs);
//即:
PyType_Type.tp_call(&PyFloat_Type, args, kwargs);
// 而在創建PyType_Type的時候,給tp_call成員傳遞的是type_call, 因此最終相當於
type_call(&PyFloat_Type, args, kwargs)

調用參數通過args和kwargs兩個對象傳遞,關於參數傳遞暫時先不展開,留到函數機制中再詳細介紹。

然后我們圍觀一下type_call函數,它位於Object/typeobject.c中。

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{	
    // 如果我們調用的是float,那么顯然這里的type就是&PyFloat_Type
    
    // 這里是聲明一個PyObject *,顯然這是要返回的實例對象的指針
    PyObject *obj;
	
    //這里的tp_new是什么估計有人已經猜到了,我們說__call__對應底層的tp_call
    //那么這里tp_new呢?然后對應Python中的__new__方法,這里是為實例對象分配空間
    if (type->tp_new == NULL) {
        PyErr_Format(PyExc_TypeError,
                     "cannot create '%.100s' instances",
                     type->tp_name);
        return NULL;
    }
	
    //通過tp_new分配空間,此時實例對象就已經創建完畢了,這里會返回其指針
    obj = type->tp_new(type, args, kwds);
    //類型檢測,暫時不用管
    obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);
    if (obj == NULL)
        return NULL;

    //判斷參數的,我們說這里的參數type是類型對象,但也可以是元類,元類也是由PyTypeObject結構體實例化得到的
    //元類在調用的時候執行的依舊是type_call,所以這里是檢測type指向的是不是PyType_Type
    //如果是的話,那么實例化得到的obj就不是實例對象了,而是類型對象,要單獨檢測一下
    if (type == &PyType_Type &&
        PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
        (kwds == NULL ||
         (PyDict_Check(kwds) && PyDict_GET_SIZE(kwds) == 0)))
        return obj;

    //tp_new應該返回相應類型對象的實例對象(的指針),后面為了方便在Python層面就不提指針了,直接用實例對象代替了
    //但如果返回的不是,那么就不會執行tp_init,而是直接將這里的obj返回
    //這里不理解的話,我們后面會細說
    if (!PyType_IsSubtype(Py_TYPE(obj), type))
        return obj;
	
    //拿到obj的類型
    type = Py_TYPE(obj);
    //執行tp_init,顯然這個tp_init就是__init__函數,這與Python中類的實例化過程是一致的。
    if (type->tp_init != NULL) {
        //執行tp_init, 設置參數
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            //執行失敗,將引入計數減1,然后將obj設置為NULL
            assert(PyErr_Occurred());
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!PyErr_Occurred());
        }
    }
    //返回obj
    return obj;
}

因此從上面我們可以看到關鍵的部分有兩個:

  • 調用類型對象的tp_new函數指針指向的函數為實例對象申請內存。
  • 調用tp_init函數指針指向的函數為實例對象進行初始化,也就是設置屬性。

所以這對應Python中的__new____init__,我們說__new__是為實例對象開辟一份內存,然后返回指向這片內存(對象)的指針,會自動傳遞給__init__中的self。

class Girl:

    def __new__(cls, name, age):
        print("__new__方法執行啦")
        # 寫法非常固定,調用object.__new__(cls)就會創建Girl的實例對象
        # 因此這里的cls指的就是這里的Girl, 但是一定要返回, 因為__new__會將自己的返回值交給__init__中的self
        return object.__new__(cls)

    def __init__(self, name, age):
        print("__init__方法執行啦")
        self.name = name
        self.age = age


g = Girl("古明地覺", 16)
print(g.name, g.age)
"""
__new__方法執行啦
__init__方法執行啦
古明地覺 16
"""

但是注意:__new__里面的參數要和__init__里面的參數保持一致,因為我們會先執行__new__,然后解釋器會將__new__的返回值和我們傳遞的參數組合起來一起傳遞給self。因此__new__里面的參數位置除了cls之外,一般都會寫*args和**kwargs。

然后再回過頭來看一下type_call中的這幾行代碼:

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{	
    //......
    //......

    //tp_new應該返回相應類型對象的實例對象(的指針),但如果返回的不是
    //那么就不會執行tp_init,而是直接將這里的obj返回
    //這里不理解的話,我們后面會細說
    if (!PyType_IsSubtype(Py_TYPE(obj), type))
        return obj;
	
    //......
    //......
}

我們說tp_new應該返回該類型對象的實例對象指針,而且一般情況下我們是不寫__new__的,會默認執行。但是我們一旦重寫了,那么必須要手動返回object.__new__(cls),那么如果我們不返回,或者返回其它的話,會怎么樣呢?

class Girl:

    def __new__(cls, *args, **kwargs):
        print("__new__方法執行啦")
        instance = object.__new__(cls)
        # 打印看看instance到底是個什么東東
        print("instance:", instance)
        print("type(instance):", type(instance))
        
        # 正確做法是將instance返回, 但是我們不返回, 而是返回個123
        return 123

    def __init__(self, name, age):
        print("__init__方法執行啦")


g = Girl()
"""
__new__方法執行啦
instance: <__main__.Girl object at 0x000002C0F16FA1F0>
type(instance): <class '__main__.Girl'>
"""

這里面有很多可以說的點,首先就是__init__里面需要兩個參數,但是我們沒有傳,卻還不報錯。原因就在於這個__init__壓根就沒有執行,因為__new__返回的不是Girl的實例對象。

通過打印instance,我們知道了object.__new__(cls)返回的就是cls的實例對象,而這里的cls就是Girl這個類本身,我們必須要返回instance,才會執行對應的__init__,否則__new__直接就返回了。我們來打印一下其返回值:

class Girl:

    def __new__(cls, *args, **kwargs):
        return 123

    def __init__(self, name, age):
        print("__init__方法執行啦")


g = Girl()
print(g, type(g))  # 123 <class 'int'>

我們看到直接打印的就是123,所以再次總結一些tp_new和tp_init之間的區別,當然也對應__new__和__init__的區別:

  • tp_new:為該類型對象的實例對象申請內存,在Python的__new__方法中通過object.__new__(cls)的方式申請,然后將其返回。
  • tp_init:tp_new的返回值會自動傳遞給self,然后為self綁定相應的屬性,也就是執行構造函數進行初始化。

但如果tp_new返回的不是對應類型的實例對象指針,比如type_call中第一個參數接收的&PyFloat_Type,但是tp_new中返回的卻是PyLongObject類型的指針,所以此時就不會執行tp_init。

以Python為例,我們Girl中的__new__應該返回Girl的實例對象才對,但實際上返回了整型,因此類型不一致,所以不會執行__init__。

所以通過類型對象去創建實例對象的整體流程如下:

  • 1. 執行類型對象的類型對象,說白了就是元類,執行元類中的type_call指向的函數;
  • 2. tp_call會調用該類型對象的tp_new指向的函數,如果tp_new為NULL(實際上肯定不會NULL,但是我們假設為NULL),那么會到tp_base指定的父類里面去尋找tp_new。在新式類當中,所有的類都繼承自object,因此最終會找到一個不為NULL的tp_new。然后通過tp_new會訪問對應類型對象中的tp_basicsize信息,繼而完成申請內存的操作。這個信息記錄着一個該對象的實例對象需要占用多大內存。在為實例對象分配空間之后,會將指向這片空間的指針交給tp_init;
  • 3. 在調用type_new完成創建對象之后,流程就會轉向PyLong_Type的tp_init,完成初始化對象的工作。當然這個tp_init也可能不被調用,原因我們上面已經分析過了;

所以我們說Python中__new__調用完了會自動調用__init__,而且還會將其返回值傳遞給__init__中的第一個參數。那是因為在type_call中先調用的tp_new,然后再調用的tp_init,同時將tp_new的返回值傳進去了。從源碼的角度再分析一遍:

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{	
    //當我們創建一個類的實例對象的時候,會去調用元類的__call__方法,所以是這里的tp_call
    //比如Girl("古明地覺", 16) 等價於 type.__call__(Girl, "古明地覺", 16)
    //所以走到了這一步
    
    //調用__new__方法, 拿到其返回值
    obj = type->tp_new(type, args, kwds);

    if (type->tp_init != NULL) {
        //調用__init__,將返回值obj傳遞給__init__中的self,並在里面設置屬性
        int res = type->tp_init(obj, args, kwds);
    return obj;
}

因此底層所表現出來的和我們在Python中看到的,是一樣的。

對象的多態性

我們說Python創建一個對象,比如PyFloatObject,會分配內存並進行初始化。然后Python內部會統一使用一個叫做PyObject*的泛型指針來保存和維護這個對象,而不是PyFloatObject *。

通過PyObject *保存和維護對象,可以實現更加抽象的上層邏輯,而不用關心對象的實際類型和實現細節。比如:哈希計算

Py_hash_t
PyObject_Hash(PyObject *v);

該對象可以計算任意對象的哈希值,而不用關心對象的類型是啥,它們都可以使用這個函數。

但是不同類型的對象,其行為也千差萬別,哈希值計算的方式也是如此,那么PyObject_Hash函數是如何解決這個問題的呢?不用想,因為元信息存儲在對應的類型對象之中,所以肯定會通過其ob_type拿到指向的類型對象。而類型對象中有一個成員叫做tp_hash,它是一個函數指針,指向的函數專門用來計算其實例對象的哈希值,我們看一下PyObject_Hash的函數定義吧,它位於Object/Object.c中。

Py_hash_t
PyObject_Hash(PyObject *v)
{	
    //Py_TYPE是一個宏,用來獲取一個PyObject *內部的ob_type,不過從名字也能看出來
    PyTypeObject *tp = Py_TYPE(v);
    //獲取對應的類型對象內部的tp_hash方法,tp_hash是一個函數指針
    if (tp->tp_hash != NULL)
        //如果tp_hash不為空,證明確實指向了具體的hash函數,那么拿到拿到函數指針之后,通過*獲取對應的函數
        //然后將PyObject *傳進去計算哈希值,返回。
        return (*tp->tp_hash)(v);
	
    //如果tp_hash為空,那么有兩種可能。1. 說明該類型對象可能還未初始化, 導致tp_hash暫時為空; 2. 說明該類型本身就不支持其"實例對象"被哈希
    // 如果是第1種情況,那么它的tp_dict、也就是屬性字典一定為空,tp_dict是動態設置的,因此它若為空,是該類型對象沒有初始化的重要特征
    //如果它不為空,說明類型對象一定已經被初始化了,所以此時tp_hash為空,就真的說明該類型不支持實例對象被哈希
    if (tp->tp_dict == NULL) {
        //如果為空,那么先進行類型的初始化
        if (PyType_Ready(tp) < 0)
            return -1;
        //然后再看是否tp_hash是否為空,為空的話,說明不支持哈希
        //不為空則調用對應的哈希函數
        if (tp->tp_hash != NULL)
            return (*tp->tp_hash)(v);
    }
    // 走到這里代表以上條件都不滿足,說明該對象不可以被hash
    return PyObject_HashNotImplemented(v);
}

函數先通過ob_type指針找到對象的類型,然后通過類型對象的tp_hash函數指針調用對應的哈希計算函數。所以PyObject_Hash根據對象的類型,調用不同的哈希函數,這不正是實現了多態嗎?

通過ob_type字段,Python在C語言的層面實現了對象的多態特性,思路跟C++中的"虛表指針"有着異曲同工之妙。

另外可能有人覺得這個函數的源碼寫的不是很精簡,比如一開始已經判斷過內部的tp_hash是否為NULL,然后在下面又判斷了一次。那么可不可以先判斷tp_dict是否為NULL,為NULL進行初始化,然后再判斷tp_hash是否NULL,不為NULL的話執行tp_hash。這樣的話,代碼會變得精簡很多。

答案是可以的,而且這種方式似乎更直觀,但是效率上不如源碼。因為我們這種方式的話,無論是什么對象,都需要判斷其類型對象中tp_dict和tp_hash是否為NULL。而源碼中先判斷tp_hash是否為NULL,不為NULL的話就不需要再判斷tp_dict了;如果tp_hash為NULL,再判斷是否tp_dict也為NULL,如果tp_dict為NULL則初始化,再進一步再判斷tp_hash是否還是NULL。所以對於已經初始化(tp_hash不為NULL)的類型對象,源碼中少了一次對tp_dict是否為NULL的判斷,所以效率會更高。

當然這並不是重點,我想說的重點是類似於先判斷tp_hash是否為空、如果不為空則直接調用這種方式,叫做CPython中的快分支。而且CPython中還有很多其它的快分支,快分支的特點就是命中率極高,可以盡早做出判斷、盡早處理。回到當前這個場景,只有當類型未被初始化的時候,才會不走快分支,而其余情況都走快分支。也就是說快分支只有在第一次調用的時候才可能不會命中,其余情況都是命中,因此沒有必要每次都對tp_dict進行判斷。所以源碼的設計是非常合理的,我們在后面分析函數調用的時候,也會看到很多類似於這樣的快分支。

再舉個生活中的栗子解釋一下快分支:好比你去見心上人,但是心上人說你今天沒有打扮,於是你又跑回去打扮一番之后再去見心上人。所以既然如此,那為什么不能先打扮完再去見心上人呢?答案是在絕大部分情況下,即使你不打扮,心上人也不會介意,只有在極少數情況下,比如心情不好,才會讓你回去打扮之后再過來。所以不打扮直接去見心上人就能牽手便屬於快分支,它的特點就是命中率極高,絕大部分都會走這個情況,所以沒必要每次都因為打扮耽誤時間,只有在極少數情況下快分支才不會命中。

對象的行為

這里說一句,關於對象我們知道Python中的類型對象和實例對象都屬於對象,但是我們更關注的是實例對象的行為。

而不同對象的行為不同,比如hash值的計算方法就不同,由類型對象中tp_hash字段決定。但除了tp_hash,PyTypeObject中還定義了很多函數指針,這些指針最終都會指向某個函數,或者為空表示不支持該操作。這些函數指針可以看做是"類型對象"中定義的操作,這些操作決定了其"實例對象"在運行時的"行為"。雖然所有類型對象在底層都是由同一個結構體PyTypeObject實例化得到的,但內部成員接收的值不同,得到的類型對象就不同;類型對象不同,導致其實例對象的行為就不同,這也正是一種對象區別於另一種對象的關鍵所在。

比如列表支持append,這說明在PyList_Type中肯定有某個函數指針,能夠找到用於列表append操作的函數。

整型支持除法操作,說明PyLong_Type中也有對應除法操作的函數指針。

整型、浮點型、字符串、元組、列表都支持加法操作,說明它們也都有對應加法操作的函數指針,並且類型不同,也會執行不同的加法操作。比如:1 + 1 = 2,"xx" + "yy" = "xxyy",不可能對字符串使用整型的加法操作。而字典不支持加法操作,說明創建PyDict_Type的時候,沒有給相應的結構體成員設置函數指針,可能傳了一個空。

而根據支持的操作不同,Python中可以將對象進行以下分類:

  • 數值型操作:比如整型、浮點型的加減乘除;
  • 序列型操作:比如字符串、列表、元組的通過索引、切片取值行為;
  • 映射型操作:比如字典的通過key映射出value,相當於y = f(x),將x傳進去映射出y;另外有一本專門講Python解釋器的書,基於Python2.5,書中的這里不叫映射型,而是叫關聯型。但我個人喜歡叫映射型,所以差不多都是一個東西,理解就可以。

而這三種操作,PyTypeObject中分別定義了三個指針。每個指針指向一個結構體實例,這個結構體實例中有大量的成員,成員也是函數指針,指向了具體的函數。

我們看一下定義:

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name;
	
    // .......
    PyNumberMethods *tp_as_number;  // 數值型相關操作
    PySequenceMethods *tp_as_sequence;   // 序列型相關操作
    PyMappingMethods *tp_as_mapping;  // 映射型相關操作
    // ......
} PyTypeObject;

我們看一下tp_as_number,它是PyNumberMethods類型的結構體指針:

//object.h
typedef struct {
    /* Number implementations must check *both*
       arguments for proper type and implement the necessary conversions
       in the slot functions themselves. */

    binaryfunc nb_add;
    binaryfunc nb_subtract;
    binaryfunc nb_multiply;
    binaryfunc nb_remainder;
    binaryfunc nb_divmod;
    ternaryfunc nb_power;
    unaryfunc nb_negative;
    unaryfunc nb_positive;
    unaryfunc nb_absolute;
    inquiry nb_bool;
    unaryfunc nb_invert;
    binaryfunc nb_lshift;
    binaryfunc nb_rshift;
    binaryfunc nb_and;
    binaryfunc nb_xor;
    binaryfunc nb_or;
    unaryfunc nb_int;
    void *nb_reserved;  /* the slot formerly known as nb_long */
    unaryfunc nb_float;

    binaryfunc nb_inplace_add;
    binaryfunc nb_inplace_subtract;
    binaryfunc nb_inplace_multiply;
    binaryfunc nb_inplace_remainder;
    ternaryfunc nb_inplace_power;
    binaryfunc nb_inplace_lshift;
    binaryfunc nb_inplace_rshift;
    binaryfunc nb_inplace_and;
    binaryfunc nb_inplace_xor;
    binaryfunc nb_inplace_or;

    binaryfunc nb_floor_divide;
    binaryfunc nb_true_divide;
    binaryfunc nb_inplace_floor_divide;
    binaryfunc nb_inplace_true_divide;

    unaryfunc nb_index;

    binaryfunc nb_matrix_multiply;
    binaryfunc nb_inplace_matrix_multiply;
} PyNumberMethods;

你看到了什么,是的,這不就是python里面的魔法方法嘛。在PyNumberMethods里面定義了作為一個數值應該支持的操作。如果一個對象能被視為數值對象,比如整數,那么在其對應的類型對象PyLong_Type中,tp_as_number -> nb_add就指定了對該對象進行加法操作時的具體行為。同樣,PySequenceMethods和PyMappingMethods中分別定義了作為一個序列對象和映射對象應該支持的行為,這兩種對象的典型例子就是list和dict。所以,只要 類型對象 提供相關 操作 , 實例對象 便具備對應的 行為 。

然而對於一種類型來說,它完全可以同時定義三個函數中的所有操作。換句話說,一個對象既可以表現出數值對象的特征,也可以表現出映射對象的特征。

class Int(int):

    def __getitem__(self, item):
        return item


a = Int(1)
b = Int(2)

print(a + b)  # 3
print(a["(嘎~嘎~嘎~)"])  # (嘎~嘎~嘎~)

看上去a[""]這種操作是一個類似於dict這樣的對象才支持的操作。從int繼承出來的Int自然是一個數值對象,但是通過重寫__getitem__這個魔法函數,可以視為指定了Int在python內部對應的PyTypeObject對象的tp_as_mapping -> mp_subscript操作。最終Int實例對象表現的像一個map一樣。歸根結底就在於PyTypeObject中允許一種類型對象同時指定多種不同的行為特征。 默認使用PyTypeObject結構體實例化出來的PyLong_Type對象所生成的實例對象是不具備list和dict的屬性特征的,但是我們繼承PyLong_Type,同時指定__getitem__,使得我們自己構建出來的類型對象所生成的實例對象,同時具備int、list(部分)、dict(部分)的屬性特征,就是因為python支持同時指定多種行為特征。

我們以浮點型為例:

//Object/floatobject.c
PyTypeObject PyFloat_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "float",
    sizeof(PyFloatObject),
    //......
    &float_as_number,                           /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    //......
};

//我們看到了該類型對象在創建時,給成員tp_as_number,傳入了一個float_as_number指針
//那么這個float_as_number指針就應該指向一個PyNumberMethods結構體實例
//而指向的結構體實例中也應該有大量和浮點數運算相關的函數指針,每個函數指針指向了浮點數運算相關的函數

static PyNumberMethods float_as_number = {
    float_add,          /* nb_add */
    float_sub,          /* nb_subtract */
    float_mul,          /* nb_multiply */
    float_rem,          /* nb_remainder */
    float_divmod,       /* nb_divmod */
    float_pow,          /* nb_power */
    // ...
};
//里面的float_add、float_sub、float_mul等等顯然都是已經定義好的函數的指針
//然后創建PyNumberMethods結構體實例float_as_number的時候,分別賦值給了成員nb_add、nb_substract、nb_multiply等等等等
//創建完浮點數相關操作的PyNumberMethods結構體實例float_as_number之后,將其指針交給PyFloat_Type中的tp_as_number成員
//所以浮點數相加的時候,會執行object -> ob_type -> tp_as_number -> nb_add, 而浮點類型對象的tp_as_number就是&float_as_number
//所以再獲取其成員nb_add的時候,拿到的就是float_add指針,然后調用float_add函數

所以PyFloat_Type是支持數值型操作的,但是我們看到tp_as_sequence和tp_as_mapping這兩個成員接收到的值則不是一個函數指針,而是0,相當於空。因此float對象、即浮點數不支持序列型操作和映射型操作,比如:pi = 3.14,我們無法使用len計算長度、無法通過索引或者切片獲取指定位置的值、無法通過key獲取value,這和我們使用Python時候的表現是一致的。

我們看到PyFloat_Type中tp_as_number指向的結構體中的nb_add成員對應的函數指針是float_add,但如果是PyLong_Type的話,那么nb_add對應的函數指針則是long_add。

不同對象,使用的操作是不同的。整型相加,使用的肯定是long_add,浮點型相加使用的是float_add。

引用計數

在c和c++中,程序員被賦予了極大的自由,可以任意的申請內存。但是權利的另一面對應着責任,程序員最后不使用的時候,必須負責將申請的內存釋放,並釋放無效指針。可以說,這一點是萬惡之源,大量內存泄漏、懸空指針、越界訪問的bug由此產生。

現代的開發語言當中都有垃圾回收機制,語言本身負責內存的管理和維護,比如C#和golang。垃圾回收機制將開發人員從維護內存分配和清理的繁重工作中解放出來,但同時也剝奪了程序員和內存親密接觸的機會,並犧牲了一定的運行效率。但好處就是提高了開發效率,並降低了bug發生的幾率。Python里面同樣具有垃圾回收機制,代替程序員進行繁重的內存管理工作,而引用計數正是垃圾收集機制的一部分。

python通過對一個對象的引用計數的管理來維護對象在內存中的存在與否。我們知道Python中每一個東西都是一個對象,都有一個ob_refcnt成員。這個成員維護這該對象的引用計數,從而也最終決定着該對象的創建與消亡。

在python中,主要是通過Py_INCREF(op)和Py_DECREF(op)兩個宏,來增加和減少一個對象的引用計數,當一個對象的引用計數減少到0后,Py_DECREF將調用該對象的析構函數來釋放該對象所占有的內存和系統資源。這個析構函數就是對象的類型對象(Py***_Type)中定義的函數指針來指定的,也就是tp_dealloc。

如果熟悉設計模式中的Observer模式,就可以看到,這里隱隱約約透着Observer模式的影子。在ob_refcnt減少到0時,將觸發對象的銷毀事件。從python的對象體系來看,各個對象提供了不同事件處理函數,而事件的注冊動作正是在各個對象對應的類型對象中完成的。

我們在研究對象的行為的時候,說了比起類型對象,我們更關注實例對象的行為。那么對於引用計數也是一樣的,只有實例對象,我們探討引用計數才是有意義的。類型對象(內置)是超越引用計數規則的,永遠都不會被析構,或者銷毀,因為它們在底層是被靜態定義好的。同理,我們自定義的類,雖然可以被回收,但是探討它的引用計數也是沒有價值的。我們以內置類型對象int為例:

# del關鍵字只能作用於變量, 不可以作用於對象
# 比如:pi = 3.14, 你可以del pi, 但是不可以del 3.14, 這是不符合語法規則的
# 而int雖然我們說它是整型的類型對象, 但這是從Python的層面
# 如果從底層來講, int它也是一個變量, 指向了對應的數據結構(PyLong_Type)
# 既然是變量, 那么就可以刪除, 但是這個刪除並不是直接刪除對象,而是將變量指向的對象的引用計數減去1,然后將這個變量也給刪掉。
# Python中的對象是否被刪除是通過其引用計數是否為0決定的, "del 變量"只是刪除了這個變量,讓這個變量不再指向該對象罷了
# 所以"del 變量"得到的結果就是我們沒辦法再使用這個變量了,這個變量就沒了,但是變量之前指向的對象是不是也沒了就看還有沒有其它的引用也指向它。
try:
    del int
except Exception as e:
    print(e)  # name 'int' is not defined

# 神奇的事情發生了, 告訴我們int這個變量沒有被定義
# 原因就在於del關鍵字不會刪除內置作用域里面的變量
# 我們看一下int的引用計數
import sys
print(sys.getrefcount(int))  # 138

驚了,居然有130多個變量在指向int,這130多個變量分別都是誰我們就無需關注了,找出這130多個變量顯然是一件很恐怖的事情。

總之,我們探討類型對象的引用計數是沒有太大意義的,而且內置類型對象是超越了引用計數的規則的,所以我們沒必要太關注,我們重心是在實例對象上。我們真正的操作也都是依賴實例對象進行操作的。

>>> import sys
>>> e = 2.71  # 創建一個新對象,顯然此時的引用計數為1
>>> sys.getrefcount(e)
2  # 估計有人好奇了,為啥引用計數是2, 難道不是1嗎?因為e這個變量作為參數傳到了sys.getrefcount這個函數里面
   # 所以函數里面的參數也指向2.71這個PyFloatObject,所以引用計數加1。當函數結束后,局部變量被銷毀,再將引用計數減1
>>>
>>> e1 = e  # 變量間的傳遞會傳遞指針,所以e1也會指向2.71這個浮點數,因此它的引用計數加1。
		   # 注意:我們說變量只是個符號,引用計數是針對變量指向的對象而言的,變量本身沒有所謂的引用計數
>>> sys.getrefcount(e)  # 此時變量指向的對象的引用計數為3(sys.getrefcount函數參數對"對象"的引用也算在內)
3
>>> sys.getrefcount(e1)  # 我們說操作變量相當於操作變量指向的對象,e和e1都指向同一個對象,所以獲取也是同一個對象的引用計數
3  # 因此結果是一樣的,都是3
>>> l = [e, e1]  # 放在容器里面,顯然列表l中多了兩個指針,這兩個指針也指向這里的PyFloatObject對象
>>> sys.getrefcount(e)  
5  # 因此結果為5
>>> del l  # 將列表刪除、或者將列表清空,那么里面的變量也就沒了,因此在刪除變量的時候,會先將變量指向的對象的引用計數減去1
>>> sys.getrefcount(e)  
3  # 所以又變成了3
>>> del e1  # 再刪除一個變量,引用計數再減1
>>> sys.getrefcount(e)
2  # 結果為2,說明外部還有一個變量在引用它,因為這個浮點數不會被回收。
>>> del e  # 再次del,此時引用計數為0,這個浮點數就真的沒了。
>>> 

另外,引用計數什么時候會加1,什么時候會減1,我們在上一篇博客中也說的很詳細了,可以去看一下。

關於引用計數,Python底層也提供了幾個宏。

#define _Py_NewReference(op) (                          \
    _Py_INC_TPALLOCS(op) _Py_COUNT_ALLOCS_COMMA         \
    _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA               \
    Py_REFCNT(op) = 1)//對於新創建的對象,引用計數為1

#define _Py_Dealloc(op) (                               \
    _Py_INC_TPFREES(op) _Py_COUNT_ALLOCS_COMMA          \
    (*Py_TYPE(op)->tp_dealloc)((PyObject *)(op)))
//引用計數為0時執行析構函數, Py_TYPE(op)->tp_dealloc獲取析構函數對應的函數指針,再通過*獲取指向的函數
//將傳入PyObject *指針,將其回收


//增加引用計數
#define Py_INCREF(op) (                         \
    _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
    ((PyObject *)(op))->ob_refcnt++) //引用計數自增1


//減少引用計數
#define Py_DECREF(op)                                   \
    do {                                                \
        PyObject *_py_decref_tmp = (PyObject *)(op);    \
        if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
        --(_py_decref_tmp)->ob_refcnt != 0)             \
            _Py_CHECK_REFCNT(_py_decref_tmp)            \
        else                                             \
            _Py_Dealloc(_py_decref_tmp);                \
            //引用計數減1,如果減完1變成了0,則執行析構函數
    } while (0)
        
//注意:Py_INCREF和Py_DECREF不可以處理NULL指針的,會報錯
//所以又有兩個宏,做了一層檢測,會判斷對象指針為NULL的情況
#define Py_XINCREF(op)                                \
    do {                                              \
        PyObject *_py_xincref_tmp = (PyObject *)(op); \
        if (_py_xincref_tmp != NULL)                  \
            Py_INCREF(_py_xincref_tmp);               \
    } while (0)

#define Py_XDECREF(op)                                \
    do {                                              \
        PyObject *_py_xdecref_tmp = (PyObject *)(op); \
        if (_py_xdecref_tmp != NULL)                  \
            Py_DECREF(_py_xdecref_tmp);               \
    } while (0)

//當然減少引用計數,除了Py_DECREF和Py_XDECREF之外,還有一個Py_CLEAR,也可以處理空指針的情況

因此這幾個宏作用如下:

  • _Py_NewReference: 接收一個對象,將其引用計數設置為1,用於新創建的對象。此外我們在定義里面還看到了一個宏Py_REFCNT,這是用來獲取對象引用計數的,當然除了Py_REFCNT之外,我們之前還見到了一個宏叫Py_TYPE,這是專門獲取對象的類型的。
  • _Py_Dealloc: 接收一個對象, 執行該對象的類型對象里面的析構函數, 來對該對象進行回收。
  • Py_INCREF: 接收一個對象, 將該對象引用計數自增1。
  • Py_DECREF: 接收一個對象, 將該對象引用計數自減1。
  • Py_XINCREF: 和Py_INCREF功能一致,但是可以處理空指針。
  • Py_XDECREF: 和Py_DECREF功能一致,但是可以處理空指針。
  • Py_CLEAR: 和Py_XDECREF類似,也可以處理空指針。

在一個對象的引用計數為0時,與該對象對應的析構函數就會被調用,但是要特別注意的是,我們剛才一致調用析構函數,會回收對象、銷毀對象或者刪除對象等等,意思都是將這個對象從內存中抹去,但是這並不意味着最終一定調用free釋放空間,換句話說就是對象沒了,但是對象占用的內存卻有可能還在。如果對象沒了,占用的內存也要釋放的話,那么頻繁申請、釋放內存空間會使Python的執行效率大打折扣(更何況Python已經背負了人們對其執行效率的不滿這么多年)。一般來說,Python中大量采用了內存對象池的技術,使用這種技術可以避免頻繁地申請和釋放內存空間。因此在析構的時候,只是將對象占用的空間歸還到內存池中。Python在操作系統之上提供了一個內存池,說白了就是對malloc進行了一層封裝,事先申請一部分內存,然后用於對象(占用內存低)的創建,這樣就不必頻繁地向操作系統請求空間了,從而大大的節省時間。這一點,在后面的Python內置類型對象(PyLongObject,PyListObject等等)的實現中,將會看得一清二楚。當然內存比較大的對象,還是需要向操作系統申請的,內存池只是用於那些內存占用比較小的對象的創建,因為這種對象顯然沒必要每次都和操作系統內核打交道。關於內存池,我們在后續系列中也會詳細說。

python對象的分類

我們之前根據支持的操作,將Python對象分成了數值型、序列型、映射型,但其實我們是可以分為5類的:

  • Fundamental對象:類型對象,如int、float、bool
  • Numeric對象:數值對象,如int實例、float實例、bool實例
  • Sequence對象:序列對象,如str實例、list實例、tuple實例
  • Mapping對象:關聯對象(映射對象),如dict實例
  • Internal對象:python虛擬機在運行時內部使用的對象,如function實例(函數)、code實例(字節碼)、frame實例(棧幀)、module實例(模塊)、method實例(方法),沒錯,函數、字節碼、棧幀、模塊、方法等等它們在底層一個一個類的實例對象。比如:函數的類型是<class 'function'>,在底層對應PyFunctionObject,那么<class 'function'>的類型對象是什么呢?顯然就是<class 'type'>啦。

關於Internal對象,我們在后續系列中會細說。

小結

這一次我們說了Python中創建對象的兩種方式,可以通過Python/C API創建,也可以通過類型對象創建。以及分析了對象的多態性,Python底層是如何通過C來實現多態,答案是通過ob_type。還說了對象的行為,對象進行某個操作的時候在底層發生了什么。最后說了引用計數,Python是通過引用計數來決定一個對象是否被回收的,但是有人知道它無法解決循環引用的問題。是的,所以Python中的gc就是為了解決這一點的,不過這也要等到介紹垃圾回收的時候再細說了。


免責聲明!

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



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