1. 程式人生 > 實用技巧 >2. 解密PyObject、PyVarObject、PyTypeObject在Python物件體系中所代表的含義,用CPython來總結Python中type和object之間的關係

2. 解密PyObject、PyVarObject、PyTypeObject在Python物件體系中所代表的含義,用CPython來總結Python中type和object之間的關係

楔子

我們在上一篇中說到了,面向物件理論中"類"和"物件"這兩個概念在Python內部都是通過"物件"實現的。"類"是一種物件,稱為"型別物件","類"例項化得到的也是"物件",稱為"例項物件"。

並且根據物件的不同特點還可以進一步分類:

  • 可變物件:物件建立之後可以本地修改;
  • 不可變物件:物件建立之後不可以本地修改;
  • 定長物件:物件所佔用的記憶體大小固定;
  • 不定長物件:物件所佔用的記憶體大小不固定;

但是"物件"在Python的底層是如何實現的呢?我們知道標準的Python直譯器是C語言實現的CPython,但C並不是一個面向物件的語言,那麼它是如何實現Python中的面向物件的呢?

首先對於人的思維來說,物件是一個比較形象的概念,但對於計算機來說,物件卻是一個抽象的概念。它並不能理解這是一個整數,那是一個字串,計算機所知道的一切都是位元組。通常的說法是:物件是資料以及基於這些資料的操作的集合。在計算機中,一個物件實際上就是一片被分配的記憶體空間,這些記憶體可能是連續的,也可能是離散的。

而Python中的任何物件在C中都對應一個結構體例項,在Python中建立一個物件,等價於在C中建立一個結構體例項。所以Python中的物件本質上就是C中malloc函式為結構體例項在堆區申請的一塊記憶體。

下面我們就來分析一下Python中的物件在C中是如何實現的,究竟生得一副什麼模樣,是三頭六臂還是烈焰紅脣。而第一步,就是下面要介紹的PyObject。

實現物件機制的基石--PyObject

Python中一切皆物件,而所有的物件都擁有一些共同的資訊(也叫頭部資訊),這些資訊就在PyObject中,PyObject是Python整個物件機制的核心,我們來看看它的定義:

//Include/object.h
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;

我們看到以上便是PyObject的內部資訊,我們先來看看_PyObject_HEAD_EXTRA,這是一個巨集,如果將其展開的話:

#ifdef Py_TRACE_REFS
/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA            \
    struct _object *_ob_next;           \
    struct _object *_ob_prev;

#define _PyObject_EXTRA_INIT 0, 0,

#else
#define _PyObject_HEAD_EXTRA
#define _PyObject_EXTRA_INIT
#endif

那麼這個巨集是做什麼的呢?這個巨集是用來實現一個名叫refchain的"雙向連結串列"的,Python會將程式中建立的所有物件都放入到這個雙向連結串列中,用於跟蹤所有活躍的堆對像。每一個物件都指向了它的前一個物件和後一個物件,如果是第一個物件,那麼它的前繼節點為NULL;如果是最後一個節點,那麼它的後繼節點為NULL。不過這個巨集僅僅是在debug下有用,所以我們目前不需要管這個巨集。

我們的重心是PyObject中的這個巨集下面的兩位老鐵:ob_refcnt和ob_type。

ob_refcnt:引用計數

ob_refcnt表示物件的引用計數,當一個物件被引用時,那麼ob_refcnt會自增1;引用解除時,ob_refcnt自減1。而一旦物件的引用計數為0時,那麼這個物件就會被回收。

那麼在哪些情況下,引用計數會加1呢?哪些情況下,引用計數會減1呢?

導致引用計數加1的情況:

  • 物件被建立:比如name = "古明地覺", 此時物件就是"古明地覺"這個字串, 建立成功時它的引用計數為1
  • 變數傳遞使得物件被新的變數引用:比如Name = name
  • 引用該物件的某個變數作為引數傳到一個函式或者類中:比如func(name)
  • 引用該物件的某個變數作為元組、列表、集合等容器的一個元素:比如lst = [name]

導致引用計數減1的情況:

  • 引用該物件的變數被顯示的銷燬:del name
  • 物件的引用指向了別的物件:name = "椎名真白"
  • 引用該物件的變數離開了它的作用域,比如函式的區域性變數在函式執行完畢的時候會被銷燬
  • 引用該物件的變數所在的容器被銷燬,或者被從容器裡面刪除

所以我們使用del刪除一個物件,並不是刪除這個物件,我們沒有這個權力,del只是使物件的引用計數減一,至於到底刪不刪是直譯器判斷物件引用計數是否為0決定的。為0就刪,不為0就不刪,就這麼簡單。

而ob_refcnt的型別是Py_ssize_t,在64位機器上直接把這個型別看成long即可(話說這都快2021年了,不會還有人用32位機器吧),因此一個物件的引用計數不能超過long所表示的最大範圍。但是顯然,如果不是吃飽了撐的寫惡意程式碼,是不可能超過這個範圍的。

ob_type:型別指標

我們說一個物件是有型別的,型別物件描述例項物件的資料和行為,而ob_type儲存的便是對應型別物件的指標,所以型別物件在底層對應的是struct _typeobject例項。從這裡我們可以看出,所有的型別物件在底層都是由同一個結構體例項化得到的,因為PyObject是所有的物件共有的,它們的ob_type指向的都是struct _typeobject。

所以不同的例項物件對應不同的結構體,但是型別物件對應的都是同一個結構體。

因此我們看到PyObject的定義非常簡單,就是一個引用計數和一個型別指標,所以Python中的任意物件都必有:引用計數和型別這兩個屬性。

實現變長物件的基石--PyVarObject

我們說PyObject是所有物件的核心,它包含了所有物件都共有的資訊,但是還有那麼一個屬性雖然不是每個物件都有,但至少有一大半的物件會有,能猜到是什麼嗎?

我們說Python中的物件根據所佔的記憶體是否固定可以分為定長物件和變長物件,而變長物件顯然有一個長度的概念,比如字串、列表、元組等等,即便是相同的例項物件,但是長度不同,所佔的記憶體也是不同的。比如:字串內部有多少個字元、元組、列表內部有多少個元素,顯然這裡的多少也是Python中很多物件的共有特徵,雖然不像引用計數和型別那樣是每個物件都必有的,但也是相當大一部分物件所具有的。

所以針對變長物件,Python底層也提供了一個結構體,因為Python很多都是變長物件。

//Include/object.h
typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;

所以我們看到PyVarObject實際上是PyObject的一個擴充套件,它在PyObject的基礎上提供了一個ob_size欄位,用於記錄內部的元素個數。比如列表,列表(PyListObject例項)中的ob_size維護的就是列表的元素個數,插入一個元素,ob_size會加1,刪除一個元素,ob_size會減1。所以我們使用len獲取列表的元素個數是一個時間複雜度為O(1)的操作,因為ob_size是時刻都和內部的元素個數保持一致,使用len獲取元素個數的時候會直接訪問ob_size。

因此在Python中,所有的變長物件都擁有PyVarObject,而所有的物件都擁有PyObject,這就使得在Python中,對"物件"的引用變得非常統一,我們只需要一個PyObject *就可以引用任意一個物件,而不需要管這個物件實際是一個什麼物件。所以在Python中,所有的變數、以及容器內部的元素,本質上都是一個PyObject *。

由於PyObject和PyVarObject要經常被使用,所以Python提供了兩個巨集,方便定義。

#define PyObject_HEAD          PyObject ob_base;
#define PyObject_VAR_HEAD      PyVarObject ob_base;

比如定長物件浮點數,在底層對應的結構體為PyFloatObject,只需在頭部PyObject的基礎上再加上一個double即可。

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

而對於變長物件列表,在底層對應的結構體是PyListObject,所以它需要在PyVarObject的基礎上再加上一個指向陣列的二級指標和一個容量即可。

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;

這上面的每一個成員都代表什麼,我們之前已經分析過了。ob_item就是指向指標陣列的二級指標,而allocated表示已經分配的容量,一旦新增元素的時候發現ob_size自增1之後會大於allocated,那麼直譯器就會對ob_item指向的指標陣列進行擴容了。更準確的說,是申請一個容量更大陣列,然後將原來指向的指標陣列內部的元素按照順序一個一個地拷貝到新的數組裡面去,並讓ob_item指向新的陣列,這一點在分析PyListObject的時候會細說。所以我們看到列表在新增元素的時候,地址是不會改變的,即使容量不夠了也沒有關係,直接讓ob_item指向新的陣列就好了,至於PyListObject物件本身的地址是不會變化的。

最後再來介紹兩個巨集定義,這個是針對於型別物件的,我們後面在介紹型別物件的時候會經常見到這兩個巨集定義。

// Include/object.h
#define PyObject_HEAD_INIT(type)        \
    { _PyObject_EXTRA_INIT              \
    1, type },


#define PyVarObject_HEAD_INIT(type, size)       \
    { PyObject_HEAD_INIT(type) size },

先看PyObject_HEAD_INIT,裡面的_PyObject_EXTRA_INIT是用來實現refchain這個雙向連結串列的,我們目前不需要管。裡面的1指的是引用計數,我們看到剛建立的時候預設是設定為1的,至於type就是該型別物件的型別了,這個是作為巨集的引數傳進來的;而PyVarObject_HEAD_INIT,則是在PyObject_HEAD_INIT的基礎之上,增加了一個size,顯然我們從名字也能看出來這個size是什麼。當然目前只是介紹這兩個巨集,先有個印象,型別物件的實現我們下面就會說。

實現型別物件的基石--PyTypeObject

通過PyObject和PyVarObject,我們看到了Python中所有物件的共有資訊以及變長物件的共有資訊。對於任何一個物件,不管它是什麼型別,內部必有引用計數(ob_refcnt)和型別指標(ob_type);對於任意一個變長物件,不管它是什麼型別,除了引用計數和型別指標之外,內部還有一個表示元素個數的ob_size。

顯然目前是沒有什麼問題,一切都是符合我們的預期的,但是當我們順著時間軸回溯的話,就會發現端倪。比如:

  • 1. 當在記憶體中建立物件、分配空間的時候,直譯器要給該物件分配多大的空間?顯然不能隨便分配,那麼該物件的記憶體資訊在什麼地方?
  • 2. 一個物件是支援相應的操作的,直譯器怎麼判斷該物件支援哪些操作呢?再比如一個整型可以和一個整型相乘,但是一個列表也可以和一個整型相乘,即使是相同的操作,但不同型別的物件執行也會有不同的結果,那麼此時直譯器又是如何進行區分的?

想都不用想,這些資訊肯定都在物件所對應的型別物件中。而且佔用的空間大小實際上是物件的一個元資訊,這樣的元資訊和其所屬型別是密切相關的,因此它一定會出現在與之對應的型別物件當中。至於支援的操作就更不用說了,我們平時自定義類的時候,方法都寫在什麼地方,顯然都是寫在類裡面,因此一個物件支援的操作顯然定義在型別物件當中。

而將一個物件和其型別物件關聯起來的,毫無疑問正是該物件內部的PyObject中的ob_type,也就是型別指標。我們通過物件的ob_type成員即可獲取指向的型別物件的指標,通過該指標可以獲取儲存在型別物件中的某些元資訊。

下面我們來看看型別物件在底層是怎麼定義的:

//Include/object.h
typedef struct _object {
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject; //_typeobject正是PyObject裡面的一個成員

// 型別物件對應的結構體
typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name;
    Py_ssize_t tp_basicsize, tp_itemsize; 
    destructor tp_dealloc;
    printfunc tp_print;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;
    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;
    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;
    PyBufferProcs *tp_as_buffer;
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */


    traverseproc tp_traverse;

    inquiry tp_clear;
    richcmpfunc tp_richcompare;

    Py_ssize_t tp_weaklistoffset;

    getiterfunc tp_iter;
    iternextfunc tp_iternext;
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    struct _typeobject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;
    unsigned int tp_version_tag;

    destructor tp_finalize;

#ifdef COUNT_ALLOCS
    Py_ssize_t tp_allocs;
    Py_ssize_t tp_frees;
    Py_ssize_t tp_maxalloc;
    struct _typeobject *tp_prev;
    struct _typeobject *tp_next;
#endif
} PyTypeObject;
#endif

型別物件在底層對應的是struct _typeobject,當然也是PyTypeObject,它裡面的成員非常非常多,我們暫時挑幾個重要的說,因為有一部分成員並不是那麼重要,我們在後續會慢慢說。

目前我們瞭解到Python中的型別物件在底層就是一個PyTypeObject例項,它儲存了例項物件的元資訊,描述物件的型別。

Python中的例項物件在底層對應不同的結構體例項,而型別物件則是對應同一個結構體例項,換句話說無論是int、str、dict等等等等,它們在C的層面都是由PyTypeObject這個結構體例項化得到的,只不過成員的值不同PyTypeObject這個結構體在例項化之後得到的型別物件也不同。

我們看一下PyTypeObject內部幾個非常關鍵的成員:

  • PyObject_VAR_HEAD:我們說這是一個巨集,對應一個PyVarObject,所以型別物件是一個變長物件。而且型別物件也有引用計數和型別,這與我們前面分析的是一致的。
  • tp_name:型別的名稱,而這是一個char *,顯然它可以是int、str、dict之類的。
  • tp_basicsize, tp_itemsize:建立對應例項物件時所需要的記憶體資訊。
  • tp_dealloc:其例項物件執行解構函式時所作的操作。
  • tp_print:其例項物件被列印時所作的操作。
  • tp_as_number:其例項物件為數值時,所支援的操作。這是一個數組指標,指向了一個指標陣列,指向的指標陣列中儲存了大量的函式指標,其函式就是整型物件可以執行的操作,比如:四則運算、左移、右移、取模等等
  • tp_as_sequence:其例項物件為序列時,所支援的操作。同樣是一個數組指標,指向一個指標陣列。
  • tp_as_mapping:其例項物件為對映時,所支援的操作。也是一個數組指標,指向一個指標陣列。
  • tp_base:繼承的基類。

我們暫時就挑這麼幾個,事實上從名字上你也能看出來這每一個成員代表的含義。而且這裡面的成員雖然多,但並非每一個型別物件都具備,比如int型別它就沒有tp_as_sequence和tp_as_mapping,所以int型別的這兩個成員的值都是0。

具體的我們就在分析具體的型別物件的時候再說吧,然後先來看看Python物件在底層都叫什麼名字吧。

  • 整型 -> PyLongObject結構體例項, int -> PyLong_Type(PyTypeObject結構體例項)
  • 字串 -> PyUnicodeObject結構體例項, str -> PyUnicode_Type(PyTypeObject結構體例項)
  • 浮點數 -> PyFloatObject結構體例項, float -> PyFloat_Type(PyTypeObject結構體例項)
  • 複數 -> PyComplexObject結構體例項, complex -> PyComplex_Type(PyTypeObject結構體例項)
  • 元組 -> PyTupleObject結構體例項, tuple -> PyTuple_Type(PyTypeObject結構體例項)
  • 列表 -> PyListObject結構體例項, list -> PyList_Type(PyTypeObject結構體例項)
  • 字典 -> PyDictObject結構體例項, dict -> PyDict_Type(PyTypeObject結構體例項)
  • 集合 -> PySetObject結構體例項, set -> PySet_Type(PyTypeObject結構體例項)
  • 不可變集合 -> PyFrozenSetObject結構體例項, frozenset -> PyFrozenSet_Type(PyTypeObject結構體例項)
  • 元類:PyType_Type(PyTypeObject結構體例項)

所以Python中的物件在底層的名字都遵循一定的標準,包括直譯器提供的Python/C API也是如此。

下面以浮點數為例,考察一下型別物件和例項物件之間的關係。

浮點型別我們說底層對應的是PyTypeObject的例項PyFloat_Type,並且浮點型別是全域性唯一的;而浮點數則是PyFloatObject例項,浮點數可以有任意個,比如:圓周率pi是一個、自然對數e又是一個。

>>> float
<class 'float'>
>>> pi = 3.14
>>> e = 2.71
>>>
>>> type(pi) is type(e) is float
True
>>>

兩個變數均指向了浮點數(PyFloatObject結構體例項),除了公共頭部欄位ob_refcnt和ob_type,專有欄位ob_fval儲存了對應的數值;浮點型別float則對應PyTypeObject結構體例項(PyFloat_Type),儲存了型別名、記憶體分配資訊以及浮點數相關操作。而將這兩者關聯起來的就是ob_type這個型別指標,它位於PyObject中,是所有物件共有的,而Python便是根據這個ob_type來判斷該物件的型別,進而獲取該物件的元資訊。

我們說變數只是一個指標,那麼int、float、dict這些是不是變數,顯然是的,函式和類也是一個變數,所以它們在底層也是一個指標。只不過這些變數是內建的,直接指向了具體的PyTypeObject例項。只是為了方便,有時我們用int、float等等,來代指指向的物件。比如:float指向了底層的PyFloat_Type,所以它其實是PyFloat_Type的指標,但為了表述方便我們會直接用float來代指PyFloat_Type。

而且型別物件在直譯器啟動的時候就已經是建立好了的,不然的話我們怎麼能夠直接用呢?型別物件建立完畢之後,直接讓float指向相應的型別物件。

我們來看一下float對應的型別物件在底層是怎麼定義的吧。

// Object/floatobject.c
PyTypeObject PyFloat_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "float",
    sizeof(PyFloatObject),
    0,
    (destructor)float_dealloc,                  /* tp_dealloc */

    // ...
    (reprfunc)float_repr,                       /* tp_repr */

    // ...
};

我們看到PyFloat_Type在原始碼中就直接被建立了,這是必須的,否則我們就沒有辦法直接訪問float這個變量了,然後先看結構體中的第4行,我們看到tp_name被初始化成了"float";第5行表示例項物件所佔的位元組數,我們看到就是一個PyFloatObject例項所佔的記憶體大小,並且顯然這個值是不會變的,說明無論建立多少個例項物件,它們的大小都是不變的,這也符合我們之前的測試結果,都是24位元組。

再往下就是一些各種操作對應的函式指標,最後我們來看一下第3行,顯然它接收的是一個PyVarObject,PyVarObject_HEAD_INIT這個巨集無需贅言,但重點是裡面的&PyType_Type,說明了float被設定成了type型別。

>>> float.__class__
<class 'type'>
>>> # 顯然這是符合我們的預期的

而且所有的型別物件(還有元類)在底層都被定義成了靜態的全域性變數,因為它們的宣告週期是伴隨著整個直譯器的,並且在任意地方都可以訪問。

型別物件的型別--PyType_Type

我們考察了float型別物件,知道它在C的層面是PyFloat_Type這個靜態全域性變數,它的型別是type。包括我們自定義的類的型別也是type,那麼type在C的層面又長啥樣呢?

在介紹PyFloat_Type的時候我們知道了type在底層對應PyType_Type,而它在"Object/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 */

    // ...
    (reprfunc)type_repr,                        /* tp_repr */

    // ...
};

我們所有的型別物件加上元類都是PyTypeObject這個結構體例項化得到的,所以它們內部的成員都是一樣的,只不過傳入的值不同,例項化之後的結果也不同,可以是PyLong_Type、可以是PyFloat_Type,也可以是這裡的PyType_Type。

但是例項物件可以由不同的結構體例項化得到,比如PyLongObject、PyUnicodeObject、PyListObject等等,而例項物件對應的結構體例項化出來所有例項都是相同型別的,比如PyLongObject例項化得到的都是整型、PyUnicodeObject例項化得到的都是字串,雖然各自維護的值可以不一樣,但是型別是一樣的。

因此這些邏輯要分清楚。

PyType_Type的內部成員和PyFloat_Type是一樣的,但是我們還是要重點看一下里面的巨集PyVarObject_HEAD_INIT,我們看到它傳遞的是一個&PyType_Type,說明它把自身的型別也設定成了PyType_Type,換句話說,PyType_Type裡面的ob_type成員指向的還是PyType_Type。

>>> type.__class__
<class 'type'>
>>> type.__class__.__class__.__class__.__class__.__class__ is type
True
>>> type(type(type(type(type(type))))) is type
True
>>>

顯然不管我們套娃多少次,最終的結果都是True,顯然這也是符合我們的預期的。

型別物件的基類--PyBaseObject_Type

我們說Python中有兩個型別物件比較特殊,一個是站在型別金字塔頂端的type,一個是站在繼承金字塔頂端的object。說完了type,我們來說說object,我們說型別物件內部的tp_base表示繼承的基類,對於PyType_Type來講,它內部的tp_base肯定是PyBaseObject_Type。

但令我們吃鯨的是,它的tp_base居然是個0,如果為0的話則表示沒有這個屬性。

0,                                          /* tp_base */

不是說type的基類是object嗎?為啥tp_base是0,事實上如果你去看PyFloat_Type的話,它內部的tp_base也是0。為0的原因就在於我們目前看到的型別物件是一個半成品,因為Python的動態性,顯然不可能在定義的時候就將所有成員屬性都設定好、然後直譯器一啟動就會得到我們平時使用的型別物件。目前看到的型別物件是一個半成品,有一部分成員屬性是在直譯器啟動之後再進行動態完善的。

至於是怎麼完善的,都有哪些成員需要直譯器啟動之後才能完善,我們後續系列會說。

而PyBaseObject_Type位於Object/object.c中,我們來一睹其芳容。

PyTypeObject PyBaseObject_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "object",                                   /* tp_name */
    sizeof(PyObject),                           /* tp_basicsize */
    0,                                          /* tp_itemsize */
    object_dealloc,                             /* tp_dealloc */

    // ...
    object_repr,                                /* tp_repr */
    // ...
};

我們看到PyBaseObject_Type的型別也被設定成了PyType_Type,而PyType_Type型別在被完善之後,它的tp_base也會指向PyBaseObject_Type。所以之前我們說Python中的type和object是同時出現的,它們的定義是需要依賴彼此的。

>>> object.__class__
<class 'type'>
>>>

注意:直譯器在完善PyBaseObject_Type的時候,是不會設定其tp_base的,因為繼承鏈必須有一個終點,否物件沿著繼承鏈進行屬性查詢的時候就會陷入死迴圈,而object已經是繼承鏈的頂點了。

>>> print(object.__base__)
None
>>>
  • object -> PyBaseObject_Type
  • object() -> PyBaseObject

小結

至此,我們算是從直譯器的角度完全理清了Python中物件體系,其實我們之前畫的圖已經將Python物件體系表達的很清晰了,如下:

我們之前花了很大一部分筆墨來從Python的角度介紹其物件體系,之所以這麼做就是為了能夠更好地理解本篇內容。如果能在Python層面上充分理解的話,那麼在CPython層面上理解也就不難了。

而且我們還介紹了PyObject、PyVarObject,並分析了Python中的type和object在底層的實現,雖然還肯定遠遠不夠,但對於當前來說已經邁出一大步了。我們在後續系列中會針對Python中型別物件進行單獨剖析,到時候再來挖掘更加細緻的內容。