Python對象的生命周期源碼學習
思考:
當我們輸入這個語句的時候,Python內部是如何去創建這個對象的?
a = 1.0
對象使用完畢,銷毀的時機又是怎麼確定的呢?
下面,我們以一個基本類型float為例,來分析對象從創建到銷毀這整個生命周期中的行為。
1 C API
Python是用C寫的,對外提供瞭API,讓用戶可以從C環境中與其交互,並且Python內部也大量使用瞭這些API。C API分為兩類:泛型API以及特型API。
泛型API:與類型無關,屬於抽象對象層,這類API的參數是PyObject *,即可以處理任意類型的對象。以PyObject_Print為例:
// 打印浮點對象 PyObject *fo = PyFloat_FromDouble(3.14); PyObject_Print(fo, stdout, 0); // 打印整數對象 PyObject *lo = PyLong_FromLong(100); PyObject_Print(lo, stdout, 0);
特型API:與類型相關,屬於具體對象層,這類API隻能作用於某種類型的對象
2 對象的創建
2.1 兩種創建對象的方式
Python內部一般通過兩種方法創建對象:
通過C API,多用於內建類型
以浮點類型為例,Python內部提供PyFloat_FromDouble,這是一個特型C API,在這個接口內部為PyFloatObject結構體變量分配內存,並初始化相關字段:
PyObject * PyFloat_FromDouble(double fval) { PyFloatObject *op = free_list; if (op != NULL) { free_list = (PyFloatObject *) Py_TYPE(op); numfree--; } else { op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject)); if (!op) return PyErr_NoMemory(); } /* Inline PyObject_New */ (void)PyObject_INIT(op, &PyFloat_Type); op->ob_fval = fval; return (PyObject *) op; }
通過類型對象,多用於自定義類型
對於自定義類型,Python就無法事先提供C API瞭,這種情況下就隻能通過類型對象中包含的元數據(分配多少內存,如何初始化等等)來創建實例對象。
由類型對象創建實例對象是一個更通用的流程,對於內建類型,除瞭通過C API來創建對象意外,同樣也可以通過類型對象來創建。以浮點類型為例,我們通過類型對象float,創建瞭一個實例對象f:
f: float = float('3.123')
2.2 由類型對象創建實例對象
思考:既然我們可以通過類型對象來創建實例對象,那麼類型對象中應該存在相應的接口。
在PyType_Type中找到瞭tp_call字段:
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 */ // ... (ternaryfunc)type_call, /* tp_call */ // ... };
因此,float(‘3.123’)在C層面就等價於:
PyFloat_Type.ob_type.tp_call(&PyFloat_Type, args. kwargs)
這裡大傢可以思考下為什麼是PyFloat_Type.ob_type——因為我們在float(‘3.14’)中是通過float這個類型對象去創建一個浮點對象,而對象的通用方法是由它對應的類型管理的,自然float的類型就是type,所以我們要找的就是type的tp_call字段。
type_call函數的C源碼:(隻列出部分)
static PyObject * type_call(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyObject *obj; // ... obj = type->tp_new(type, args, kwds); obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL); if (obj == NULL) return NULL; // ... type = Py_TYPE(obj); if (type->tp_init != NULL) { int res = type->tp_init(obj, args, kwds); if (res < 0) { assert(PyErr_Occurred()); Py_DECREF(obj); obj = NULL; } else { assert(!PyErr_Occurred()); } } return obj; }
其中有兩個關鍵的步驟:(這兩個步驟大傢應該是很熟悉的)
- 調用類型對象的tp_new函數指針,用於申請內存;
- 如果類型對象的tp_init函數指針不為空,則會對對象進行初始化。
總結:(以float為例)
- 調用float,Python最終執行的是其類型對象type的tp_call指針指向的type_call函數。
- type_call函數調用float的tp_new函數為實例對象分配內存空間。
- type_call函數必要時進一步調用tp_init函數對實例對象進行初始化。
圖示如下:
3 對象的多態性
通過類型對象創建實例對象,最後會落實到調用type_call函數,其中保存具體對象時,使用的是PyObject *obj,並沒有通過一個具體的對象(例如PyFloatObject)來保存。這樣做的好處是:可以實現更抽象的上層邏輯,而不用關心對象的實際類型和實現細節。(記得當初從C語言的面向過程向Java中的面向對象過度的時候,應該就是從結構體)
以對象哈希值計算為例,有這樣一個函數接口:
Py_hash_t PyObject_Hash(PyObject *v) { // ... }
對於浮點數對象和整數對象:
PyObject *fo = PyFloatObject_FromDouble(3.14); PyObject_Hash(fo); PyObject *lo = PyLongObject_FromLong(100); PyObject_Hash(lo);
可以看到,對於浮點數對象和整數對象,我們計算對象的哈希值時,調用的都是PyObject_Hash()這個函數,但是對象類型不同,其行為是有區別的,哈希值計算也是如此。
那麼在PyObject_Hash函數內部是如何區分的呢?
PyObject_Hash()函數具體邏輯:
Py_hash_t PyObject_Hash(PyObject *v) { PyTypeObject *tp = Py_TYPE(v); if (tp->tp_hash != NULL) return (*tp->tp_hash)(v); /* To keep to the general practice that inheriting * solely from object in C code should work without * an explicit call to PyType_Ready, we implicitly call * PyType_Ready here and then check the tp_hash slot again */ if (tp->tp_dict == NULL) { if (PyType_Ready(tp) < 0) return -1; if (tp->tp_hash != NULL) return (*tp->tp_hash)(v); } /* Otherwise, the object can't be hashed */ return PyObject_HashNotImplemented(v); }
函數會首先通過Py_TYPE找到對象的類型,然後通過類型對象的tp_hash函數指針來調用對應的哈希計算函數。
即:PyObject_Hash()函數根據對象的類型,調用不同的函數版本,這就是多態。
4 對象的行為
除瞭tp_hash字段,PyTypeObject結構體還定義瞭很多函數指針,這些指針最終都會指向某個函數,或者為空。我們可以把這些函數指針看作是類型對象中定義的操作,這些操作決定瞭對應的實例對象在運行時的行為。
雖然不同的類型對象中保存瞭對應實例對象共有的行為,但是不同類型的對象也會存在一些共性。例如:整數對象和浮點數對象都支持加減乘除等擦歐總,元組對象和列表對象都支持下標操作。因此,我們以行為為分類標準,對對象進行分類:
Python以此為依據,為每個類別都定義瞭一個標準操作集:
- PyNumberMethods結構體定義瞭數值型操作
- PySequenceMethods結構體定義瞭序列型操作
- PyMappingMethods結構體定義瞭關聯型操作
如果類型對象提供瞭相關的操作集,則對應的實例對象就具備對應的行為:
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 */ // ... PyNumberMethods *tp_as_number; PySequenceMethods *tp_as_sequence; PyMappingMethods *tp_as_mapping; // ... } PyTypeObject;
以float為例,類型對象PyFloat_Type的這三個字段是這樣初始化的:
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對象支持數值型操作,不支持序列型操作和關聯型操作。
5 引用計數
在Python中,很多場景都涉及引用計數的調整:
- 變量賦值
- 函數參數傳遞
- 屬性操作
- 容器操作
引用計數是Python生命周期中很關鍵的一個知識點,後續我會用一個單獨的章節來介紹,這裡咱們先按下不表,更多關於Python對象生命周期的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Python對象的底層實現源碼學習
- python源碼剖析之PyObject詳解
- Python內建類型bytes深入理解
- Python 調用C++封裝的進一步探索交流
- python 中sys.getsizeof的用法說明