首頁 > 軟體

Python物件的底層實現原始碼學習

2022-05-18 10:01:59

在“Python原始碼學習筆記:Python萬物皆物件”中,我們對Python的物件型別體系有了一定的認識,這篇部落格將從原始碼層面來介紹Python中萬物皆物件的底層實現。

1. PyObject:物件的基石

在Python直譯器的C層面,一切物件都是以PyObject為基礎的

C原始碼如下:

typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
} PyObject;

原始碼解讀:

_PyObject_HEAD_EXTRA:主要用於實現雙向連結串列(分析原始碼時暫時忽略)

ob_refcnt:參照計數,用於垃圾回收機制,當這個引數減少為0時即代表物件要被刪除了(Py_ssize_t當作int或long即可,感興趣的話可以去看下它的定義)

ob_type:型別指標,指向物件的型別物件(PyTypeObject,稍後介紹),型別物件描述範例物件的資料及行為。如PyLongObject的ob_type指向的就是PyLong_Type

2. PyVarObject:變長物件的基礎

PyVarObject與PyObject相比只多了一個屬性ob_size,它指明瞭邊長物件中有多少個元素

C原始碼如下:

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

定長物件和變長物件的大致結構圖示如下:

宏定義:對於具體物件,視其大小是否固定,需要包含頭部PyObject或PyVarObject,為此,標頭檔案准備了兩個宏定義,方便其他物件使用:

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

2.1 浮點物件

這裡簡單的以浮點物件作為定長物件的例子,介紹一下相關概念,後續會詳細分析float物件的原始碼。

對於大小固定的浮點物件,需要在PyObject頭部的基礎上,用一個雙精度浮點數double加以實現:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

圖示如下:

2.2 列表物件

這裡簡單的以列表物件作為變長物件的例子,介紹一下相關概念,後續會詳細分析list物件的原始碼。

對於大小不固定的列表物件,需要在PyVarObject頭部的基礎上,用一個動態陣列加以實現,陣列儲存了列表包含的物件的指標,即PyObject指標:

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

原始碼解讀:

ob_item:指向動態陣列的指標,陣列中儲存元素物件指標

allocated:動態陣列的總長度,即列表當前的“容量”

ob_size:當前元素個數,即列表當前的長度(這裡的長度是指:列表包含n個元素,則長度為n)

圖示如下:

3. PyTypeObject:型別的基石

問題:不同型別的物件所需儲存空間不同,建立物件時從哪得知儲存資訊呢?以及如何判斷一個給定物件支援哪些操作呢?

注意到,PyObject結構體中包含一個指標ob_type,指向的就是型別物件,其中就包含了上述問題所需要的資訊

C原始碼如下:(只列出了部分,後續會結合具體型別進行分析)

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
    /* Methods to implement standard operations */
    destructor tp_dealloc;
    printfunc tp_print
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    // ...
    /* Attribute descriptor and subclassing stuff */
    PyObject *tp_bases;
	// ...
} PyTypeObject;

原始碼解讀:

PyObject_VAR_HEAD表示PyTypeObject是變長物件

tp_name:型別名稱

tp_basicsize、tp_itemsize:建立範例物件時所需的記憶體資訊

tp_print、tp_getattr等:表示該型別支援的相關操作資訊

tp_bases:指向基礎類別物件,表示型別的繼承資訊

PyTypeObject就是型別物件在C層面的表示形式,對應物件導向中”類“的概念,其中儲存著物件的”元資訊“(即一類物件的操作、資料等)。

下面以浮點型別為例,列出了PyFloatObject和PyTypeObject之間的關係結構圖示:(其中兩個浮點範例物件都是PyFloatObject結構體,浮點型別物件float是一個PyTypeObject結構體變數)

由於浮點型別物件唯一,在C語言層面作為一個全域性變數靜態定義即可。C原始碼如下:(只列出了部分)

PyTypeObject PyFloat_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "float",
    sizeof(PyFloatObject),
    0,
    (destructor)float_dealloc,                  /* tp_dealloc */
    // ...
    (reprfunc)float_repr,                       /* tp_repr */
    // ...
};

原始碼解讀:

第二行PyVarObject_HEAD_INIT(&PyType_Type, 0):初始化了ob_refcnt、ob_type、ob_sie三個欄位,其中ob_type指向了PyType_Type(稍後會繼續介紹,它就是type),即:float的型別是type

第三行"float":將tp_name欄位初始化為型別名稱float

4. PyType_Type:型別的型別

通過PyFloat_Type的ob_type欄位,我們找到了type所對應的C語言層面結構體變數:PyType_Type,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物件都是通過這個PyType_Type建立的。在第二行PyVarObject_HEAD_INIT(&PyType_Type, 0)中,PyType_Type把自己的ob_type欄位設定成了它自己,即type的型別是type

把PyType_Type加入到結構圖中,圖示如下:

5. PyBaseObject_Type:型別之基

object是另外一個特殊的型別,它是所有型別的基礎類別。如果要找到object對應的結構體,我們可以通過PyFloat_Type的tp_base欄位來尋找,因為它指向的就是float的基礎類別object。但是我們檢視原始碼發現,PyFloat_Type中並沒有初始化tp_base欄位:

同樣地,我們檢視Objects資料夾下的各種不同型別所對應的結構體,發現tp_base欄位均沒有初始化,於是尋找將tp_base欄位初始化的函數:

void
_Py_ReadyTypes(void)
{
    if (PyType_Ready(&PyBaseObject_Type) < 0)
        Py_FatalError("Can't initialize object type");
    if (PyType_Ready(&PyType_Type) < 0)
        Py_FatalError("Can't initialize type type");
    // ...
    if (PyType_Ready(&PyFloat_Type) < 0)
        Py_FatalError("Can't initialize float type");
    // ...
}

_Py_ReadyTypes中統一呼叫了PyType_Ready()函數,為各種型別設定tp_base欄位:

int
PyType_Ready(PyTypeObject *type)
{
    // ...
    /* Initialize tp_base (defaults to BaseObject unless that's us) */
    base = type->tp_base;
    if (base == NULL && type != &PyBaseObject_Type) {
        base = type->tp_base = &PyBaseObject_Type;
        Py_INCREF(base);
    }
    // ...
}

可以看到,PyType_Ready在初始化tp_base欄位時,對於PyBaseObject_Type,不會設定tp_base欄位,即object是沒有基礎類別的,這就是為了保證繼承鏈有一個終點。

PyBaseObject_Type原始碼如下:(只列出了部分)

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 */
    // ...
    0,                                          /* tp_base */
    // ...
};

原始碼解讀:

第二行PyVarObject_HEAD_INIT(&PyType_Type, 0):把ob_type設定為PyType_Type,即object的型別是type

將PyBaseObject_Type加入到結構圖中,圖示如下:

6. 補充

object的型別是type,type的基礎類別是object。先有雞還是先有蛋?

答:

前面我們提到,在各種型別對應的C語言結構體變數初始化的時候,tp_base欄位都是沒有設定具體值的,直到_Py_ReadyTypes()函數執行時,才通過PyType_Ready()去初始化各型別的tp_base。

在PyBaseObject_Type初始化時,會將ob_tyep欄位設定為PyType_Type,即object的型別為type;在_Py_ReadyTypes函數中,會通過PyType_Ready()設定PyType_Type的tp_base欄位為PyBaseObject_Type。所以這裡本質上不是一個先有雞還是先有蛋的問題。

PyTypeObject儲存元資訊:某種型別的範例物件所共有的資訊儲存在型別物件中,範例物件所特有的資訊儲存在範例物件中。以float為例:

  • 無論是3.14,還是2.71,作為float物件,它們都支援加法運算,因此加法處理常式的指標就會儲存在型別物件中,即float中。
  • 而這兩個float物件的具體值都是各自特有的,因此具體數值會通過一個double型別的欄位儲存在範例物件中。

以上就是Python物件的底層實現原始碼學習的詳細內容,更多關於Python物件底層的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com