Python對象的生命周期源碼學習
思考:
當我們輸入這個語句的時候,Python內(nèi)部是如何去創(chuàng)建這個對象的?
a = 1.0
對象使用完畢,銷毀的時機又是怎么確定的呢?
下面,我們以一個基本類型float為例,來分析對象從創(chuàng)建到銷毀這整個生命周期中的行為。
1 C API
Python是用C寫的,對外提供了API,讓用戶可以從C環(huán)境中與其交互,并且Python內(nèi)部也大量使用了這些API。C API分為兩類:泛型API以及特型API。
泛型API:與類型無關,屬于抽象對象層,這類API的參數(shù)是PyObject *,即可以處理任意類型的對象。以PyObject_Print為例:
// 打印浮點對象 PyObject *fo = PyFloat_FromDouble(3.14); PyObject_Print(fo, stdout, 0); // 打印整數(shù)對象 PyObject *lo = PyLong_FromLong(100); PyObject_Print(lo, stdout, 0);
特型API:與類型相關,屬于具體對象層,這類API只能作用于某種類型的對象
2 對象的創(chuàng)建
2.1 兩種創(chuàng)建對象的方式
Python內(nèi)部一般通過兩種方法創(chuàng)建對象:
通過C API,多用于內(nèi)建類型
以浮點類型為例,Python內(nèi)部提供PyFloat_FromDouble,這是一個特型C API,在這個接口內(nèi)部為PyFloatObject結(jié)構(gòu)體變量分配內(nèi)存,并初始化相關字段:
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了,這種情況下就只能通過類型對象中包含的元數(shù)據(jù)(分配多少內(nèi)存,如何初始化等等)來創(chuàng)建實例對象。
由類型對象創(chuàng)建實例對象是一個更通用的流程,對于內(nèi)建類型,除了通過C API來創(chuàng)建對象意外,同樣也可以通過類型對象來創(chuàng)建。以浮點類型為例,我們通過類型對象float,創(chuàng)建了一個實例對象f:
f: float = float('3.123')2.2 由類型對象創(chuàng)建實例對象
思考:既然我們可以通過類型對象來創(chuàng)建實例對象,那么類型對象中應該存在相應的接口。
在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這個類型對象去創(chuàng)建一個浮點對象,而對象的通用方法是由它對應的類型管理的,自然float的類型就是type,所以我們要找的就是type的tp_call字段。
type_call函數(shù)的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;
}
其中有兩個關鍵的步驟:(這兩個步驟大家應該是很熟悉的)
- 調(diào)用類型對象的tp_new函數(shù)指針,用于申請內(nèi)存;
- 如果類型對象的tp_init函數(shù)指針不為空,則會對對象進行初始化。
總結(jié):(以float為例)
- 調(diào)用float,Python最終執(zhí)行的是其類型對象type的tp_call指針指向的type_call函數(shù)。
- type_call函數(shù)調(diào)用float的tp_new函數(shù)為實例對象分配內(nèi)存空間。
- type_call函數(shù)必要時進一步調(diào)用tp_init函數(shù)對實例對象進行初始化。
圖示如下:

3 對象的多態(tài)性
通過類型對象創(chuàng)建實例對象,最后會落實到調(diào)用type_call函數(shù),其中保存具體對象時,使用的是PyObject *obj,并沒有通過一個具體的對象(例如PyFloatObject)來保存。這樣做的好處是:可以實現(xiàn)更抽象的上層邏輯,而不用關心對象的實際類型和實現(xiàn)細節(jié)。(記得當初從C語言的面向過程向Java中的面向?qū)ο筮^度的時候,應該就是從結(jié)構(gòu)體)
以對象哈希值計算為例,有這樣一個函數(shù)接口:
Py_hash_t
PyObject_Hash(PyObject *v)
{
// ...
}
對于浮點數(shù)對象和整數(shù)對象:
PyObject *fo = PyFloatObject_FromDouble(3.14); PyObject_Hash(fo); PyObject *lo = PyLongObject_FromLong(100); PyObject_Hash(lo);
可以看到,對于浮點數(shù)對象和整數(shù)對象,我們計算對象的哈希值時,調(diào)用的都是PyObject_Hash()這個函數(shù),但是對象類型不同,其行為是有區(qū)別的,哈希值計算也是如此。
那么在PyObject_Hash函數(shù)內(nèi)部是如何區(qū)分的呢?
PyObject_Hash()函數(shù)具體邏輯:
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);
}
函數(shù)會首先通過Py_TYPE找到對象的類型,然后通過類型對象的tp_hash函數(shù)指針來調(diào)用對應的哈希計算函數(shù)。
即:PyObject_Hash()函數(shù)根據(jù)對象的類型,調(diào)用不同的函數(shù)版本,這就是多態(tài)。
4 對象的行為
除了tp_hash字段,PyTypeObject結(jié)構(gòu)體還定義了很多函數(shù)指針,這些指針最終都會指向某個函數(shù),或者為空。我們可以把這些函數(shù)指針看作是類型對象中定義的操作,這些操作決定了對應的實例對象在運行時的行為。
雖然不同的類型對象中保存了對應實例對象共有的行為,但是不同類型的對象也會存在一些共性。例如:整數(shù)對象和浮點數(shù)對象都支持加減乘除等擦歐總,元組對象和列表對象都支持下標操作。因此,我們以行為為分類標準,對對象進行分類:

Python以此為依據(jù),為每個類別都定義了一個標準操作集:
- PyNumberMethods結(jié)構(gòu)體定義了數(shù)值型操作
- PySequenceMethods結(jié)構(gòu)體定義了序列型操作
- PyMappingMethods結(jié)構(gòu)體定義了關聯(lián)型操作
如果類型對象提供了相關的操作集,則對應的實例對象就具備對應的行為:
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對象支持數(shù)值型操作,不支持序列型操作和關聯(lián)型操作。
5 引用計數(shù)
在Python中,很多場景都涉及引用計數(shù)的調(diào)整:
- 變量賦值
- 函數(shù)參數(shù)傳遞
- 屬性操作
- 容器操作
引用計數(shù)是Python生命周期中很關鍵的一個知識點,后續(xù)我會用一個單獨的章節(jié)來介紹,這里咱們先按下不表,更多關于Python對象生命周期的資料請關注腳本之家其它相關文章!
相關文章
Flask Paginate實現(xiàn)表格分頁的使用示例
flask_paginate是Flask框架的一個分頁擴展,用于處理分頁相關的功能,本文就來介紹一下Flask Paginate實現(xiàn)表格分頁的使用示例,感興趣的可以了解一下2023-11-11
Python使用add_subplot與subplot畫子圖操作示例
這篇文章主要介紹了Python使用add_subplot與subplot畫子圖操作,涉及Python使用matplotlib模塊進行圖形繪制的相關操作技巧,需要的朋友可以參考下2018-06-06
Python cookbook(數(shù)據(jù)結(jié)構(gòu)與算法)實現(xiàn)對不原生支持比較操作的對象排序算法示例
這篇文章主要介紹了Python cookbook(數(shù)據(jù)結(jié)構(gòu)與算法)實現(xiàn)對不原生支持比較操作的對象排序算法,結(jié)合實例形式分析了Python針對類實例進行排序相關操作技巧,需要的朋友可以參考下2018-03-03
Python Lambda函數(shù)使用總結(jié)詳解
這篇文章主要介紹了Python Lambda函數(shù)使用總結(jié)詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-12-12

