1. 程式人生 > 其它 >1.2 Python3 物件原始碼剖析

1.2 Python3 物件原始碼剖析

物件的基石,PyObject

上一章節探討了python一切皆物件的由來。在python中,物件可分為型別物件和例項物件,而根據不同的物件特性,又可細分為可變、不可變、定長、不定長等。

Python是由C語言實現的,那麼物件在Python內部是如何實現的?

在Python內部,所有物件均是由PyObject結構體表示,物件引用則是指標PyObject*。在Python原始碼include/object.h中,定義了PyObject結構體。(以Python3.10.0為例)

/* Nothing is actually declared to be a PyObject, but every pointer to
 * a Python object can be cast to a PyObject*.  This is inheritance built
 * by hand.  Similarly every pointer to a variable-size Python object can,
 * in addition, be cast to PyVarObject*.
 */
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
} PyObject;

以上程式碼定義了一個結構體_object以及結構體變數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

根據註釋和程式碼解讀,可以瞭解當Py_TRACES_REFS有定義,則將巨集定義為兩個指標*_ob_next和*_ob_prev。這兩個指標實現了雙向連結串列,用於跟蹤所有活躍堆物件。通常情況下不啟用,這裡不做深入瞭解。

PyObject結構體中,還包含兩個成員:

  • 引用計數(ob_refcnt):物件被其他地方引用時加一,引用解除時減一,當引用計數為零,便可將物件回收(垃圾回收的主要機制之一)。
  • 型別指標(ob_type):指向物件的型別物件,型別物件描述例項物件的資料和行為。

繼續往下閱讀object.h程式碼,可以看到結構體變數PyVarObject。

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

這是Python變長物件所對應的C結構體,在原先的PyObject基礎上加入了長度資訊ob_size,用於記錄元素的個數。

在物件中,定長與變長是二選一的屬性,如果不是定長就是變長,也就是說在物件的結構體中,需要包含頭部PyObject或PyvarObject。為此,標頭檔案特地準備了兩個巨集定義,分別表示定長巨集與不定長巨集:

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

各物件結構組成可概況如下:

以大小固定的浮點物件而言,只需要在PyObject頭部基礎上,新增一個item即可,浮點物件的item使用一個雙精度浮點數double加以實現。

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

而對於變長的列表物件而言,則需要一個PyVarObject以及動態陣列。

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

其中三個關鍵欄位含義如下:

  • ob_item,指向動態陣列的指標,陣列儲存元素物件指標;
  • allocated,動態陣列總長度,表示當前列表的容量,在Python中,如果列表容量不足時,會自動擴容,這個需要與列表實際的元素數量區分開;
  • ob_size,當前列表中的元素個數,即列表當前的長度(len(ob))。

型別的基石,PyTypeObject

從物件的基礎結構體PyObject,可以瞭解所有物件的共有資訊。對於記憶體中的任一物件,不管是何型別,肯定會存在 引用計數、型別指標 以及變長物件特有的 元素個數。

但以我們對Python物件的瞭解,PyObject無法解決以下問題:

  • 不同型別的物件所需記憶體空間不同,建立物件時從哪裡得知記憶體資訊?
  • 對於給定物件,怎麼判斷它支援什麼操作?

事實上,這些資訊稱為物件的元資訊,由一個獨立實體儲存,與物件所屬型別密切相關。還記得PyObject內包含一個指標ob_type嗎?該指標指向了一個型別物件,該型別物件便是PyTypeObject。

PyTypeObject的程式碼在cpython/object.h檔案中,部分程式碼如下:

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;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;

    // ...
    

    // Strong reference on a heap type, borrowed reference on a static type
    struct _typeobject *tp_base;
    
    // ...
};

重要欄位如下:

  • PyObject_VAR_HEAD:型別物件是一個變長物件;
  • 型別名稱:tp_name欄位;
  • 型別的繼承資訊,例如tp_base欄位指向基類物件;
  • 建立例項物件所需要的記憶體資訊,即tp_basicsize和tp_itemsize欄位;
  • 該型別支援的相關操作資訊,即tp_getattr、tp_setattr等函式指標;

以浮點為例,通過is關鍵字判斷型別物件和例項物件在記憶體中(id)的形態和關係:

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

float為浮點型別物件,系統中是唯一的,儲存了所有浮點例項物件的元資訊。所以例項物件pi和e的type均是float物件。

各物件的關係在記憶體中的形式如下:

如圖可見,兩個浮點例項物件都是PyFloatObject結構體,除了公共頭部欄位ob_refcnt和ob_type,專有欄位ob_val儲存了對應的數值。其中ob_type欄位指向了float的型別物件(也是一個PyTyoeObject的結構體),這裡儲存了型別名、記憶體分配資訊以及浮點相關操作等。Python便是依據物件型別,進而得知物件元資訊。注意,這裡float、pi以及e等變數只是一個指向實際物件的指標。

浮點型別物件是全域性唯一的,在C語言層面上作為一個全域性變數靜態定義即可。浮點型別物件PyFloat_Type程式碼位於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結構體中,第二行初始化了ob_refcnt、ob_type以及ob_size三個欄位;第三行將tp_name欄位初始化成型別名稱float;再往下就是各種操作的函式指標。

需要注意到第二行中ob_type指標指向PyType_Type,這也是一個靜態定義的全域性變數。由此可見,代表“型別的型別”即type的那個物件應該就是PyType_Type了。

型別的型別,PyType_Type

上一節瞭解了,所有物件的元型別都是type,這裡的PyType_Type的就是所說的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 */
    
    // ...
};

內建型別和自定義類對應的PyTypeObject物件都是通過這個PyType_Type建立的。PyType_Type在Python的型別機制中是一個非常重要的物件,是所有型別的型別,稱為元型別(meta class)。

型別之基,PyBaseObject_Type

在檔案Object/typeobject.c中,可以檢視PyBaseObject_Type的程式碼:

PyTypeObject PyBaseObject_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "object",                                   /* tp_name */
    sizeof(PyObject),                           /* tp_basicsize */
    
    // ....
};

根據第二行的程式碼,object的型別也是type,此外,object就是繼承鏈的末端,沒有設定具體的tp_base值。

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

至此,我們大致清楚了Python物件體系中所有實體以及關係。