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物件體系中所有實體以及關係。