Python float对象深度解析
float结构体
Include/floatobject.h 中定义
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
使用了定长对象共用的头部,定义了double类型的字段ob_fval存储浮点值。
float类型对象
Objects/floatobject.c 中定义
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float",
sizeof(PyFloatObject),
0,
(destructor)float_dealloc, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
(reprfunc)float_repr, /* tp_repr */
&float_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)float_hash, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
_Py_TPFLAGS_MATCH_SELF, /* tp_flags */
float_new__doc__, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
float_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
float_methods, /* tp_methods */
0, /* tp_members */
float_getset, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
float_new, /* tp_new */
.tp_vectorcall = (vectorcallfunc)float_vectorcall,
};
PyFloat_Type中保存了浮点对象的元信息:
- tp_name 保存类型名称,常量float
- tp_dealloc、tp_init、tp_alloc、tp_new 负责对象的创建、销毁
- tp_repr 生成语法字符串表示形式的函数
- tp_str 生成普通字符串表示形式的函数
- tp_as_number 数值操作集
- tp_hash 哈希值生成函数
浮点对象的创建
通过"Python对象的一生"一文中我们知道,调用类型对象 float 创建实例对象,Python 执行的是 type 类型对象中的 tp_call 函数。 tp_call 函数进而调用 float 类型对象的 tp_new 函数创建实例对象, 再调用 tp_init 函数对其进行初始化,如图所示
从上面看到tp_init函数指针为空。那怎么初始化呢?float是一种简单的对象,初始化只需要一个赋值语句,在tp_new中进行了初始化
static PyObject *
float_new_impl(PyTypeObject *type, PyObject *x)
/*[clinic end generated code: output=ccf1e8dc460ba6ba input=f43661b7de03e9d8]*/
{
if (type != &PyFloat_Type) {
if (x == NULL) {
x = _PyLong_GetZero();
}
return float_subtype_new(type, x); /* Wimp out */
}
if (x == NULL) {
return PyFloat_FromDouble(0.0);
}
/* If it's a string, but not a string subclass, use
PyFloat_FromString. */
if (PyUnicode_CheckExact(x))
return PyFloat_FromString(x);
return PyNumber_Float(x);
}
这是通过通用流程类型对象创建实例对象,我们还可以通过C API来创建:
PyObject *
PyFloat_FromDouble(double fval); // 通过浮点值创建浮点对象
PyObject *
PyFloat_FromString(PyObject *v); // 通过字符串对象创建浮点对象
PyFloat_FromDouble
PyObject *
PyFloat_FromDouble(double fval)
{
PyFloatObject *op;
#if PyFloat_MAXFREELIST > 0
struct _Py_float_state *state = get_float_state();
// 为对象分配内存空间,优先使用空闲对象缓存池
op = state->free_list;
if (op != NULL) {
#ifdef Py_DEBUG
// PyFloat_FromDouble() must not be called after _PyFloat_Fini()
assert(state->numfree != -1);
#endif
state->free_list = (PyFloatObject *) Py_TYPE(op);
state->numfree--;
OBJECT_STAT_INC(from_freelist);
}
else
#endif
{
op = PyObject_Malloc(sizeof(PyFloatObject));
if (!op) {
return PyErr_NoMemory();
}
}
_PyObject_Init((PyObject*)op, &PyFloat_Type); // 初始化对象类型字段ob_type以及引用计数字段ob_refcnt
op->ob_fval = fval; // 将ob_fval字段初始化为指定的浮点值
return (PyObject *) op;
}
- 为对象分配内存空间,优先使用空闲对象缓存池
- 初始化对象类型字段ob_type以及引用计数字段ob_refcnt
- 将ob_fval字段初始化为指定的浮点值
对象的销毁
Python 解释器底层通过维护一个叫做“引用计数”的技术,来跟踪和管理对象的内存。每个 Python 对象都有一个属性叫做 ob_refcnt
,表示该对象当前被引用的次数。当一个对象被创建时,它的引用计数会初始化为 1;而每当有一个新的指针指向同一个对象时,该对象的引用计数就会增加 1。相应地,当一个指针不再指向某个对象时(比如指针超出了作用域、或者被重新赋值给另一个对象),该对象的引用计数就会减少 1。
Python 解释器底层利用这种引用计数技术来自动管理内存,并在恰当的时候自动回收不再被使用的对象所占用的内存。当一个对象的引用计数归零时,Python 解释器就知道该对象没有被任何指针所指向,因此可以安全地销毁该对象并释放其占用的内存。
Python通过_Py_Dealloc回收对象
Objects/object.c
void
_Py_Dealloc(PyObject *op)
{
PyTypeObject *type = Py_TYPE(op);
destructor dealloc = type->tp_dealloc;
// ......
}
可以看到实际上调用的是tp_dealloc。对于float将调用float_dealloc
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float",
sizeof(PyFloatObject),
0,
(destructor)float_dealloc, /* tp_dealloc */
// ......
}
空闲对象缓存池
浮点运算背后涉及大量的临时对象创建和销毁
a = 2 * 3 ** 3
先计算3的立方,27由一个临时对象保存,比如a1,然后计算2与a1的乘积,最终得到结果53并赋值给变量a;最后,销毁临时对象a1。创建对象需要分配内存,销毁对象需要回收内存,大量临时对象的创建和销毁,回进行大量的内存分配回收操作,这当然不行。Python在float对象销毁之后并没有急于回收,而是放入空闲链表,后续创建浮点对象时,先到空闲链表中取,节省的内存的开销。
先看看Objects/floatobject.c
PyObject *
PyFloat_FromDouble(double fval)
{
PyFloatObject *op;
#if PyFloat_MAXFREELIST > 0
struct _Py_float_state *state = get_float_state();
// 为对象分配内存空间,优先使用空闲对象缓存池
op = state->free_list;
if (op != NULL) {
#ifdef Py_DEBUG
// PyFloat_FromDouble() must not be called after _PyFloat_Fini()
assert(state->numfree != -1);
#endif
state->free_list = (PyFloatObject *) Py_TYPE(op);
state->numfree--;
OBJECT_STAT_INC(from_freelist);
}
else
#endif
{
op = PyObject_Malloc(sizeof(PyFloatObject));
if (!op) {
return PyErr_NoMemory();
}
}
_PyObject_Init((PyObject*)op, &PyFloat_Type); // 初始化对象类型字段ob_type以及引用计数字段ob_refcnt
op->ob_fval = fval; // 将ob_fval字段初始化为指定的浮点值
return (PyObject *) op;
}
可以看到,先判断free_list是否为空,如果为非空,取出头节点备用,free_list指向第二个节点(这里看代码调用的是Py_TYPE(),也就是op的ob_type字段,也就是第二个节点),并将numfree减1,如果是空,则调用PyObject_Malloc分配内存,最后通过_PyObject_Init初始化。
源码中看到PyFloat_MAXFREELIST、free_list,我们看看在源码中的定义
#ifndef PyFloat_MAXFREELIST
# define PyFloat_MAXFREELIST 100 // 限制空闲链表的最大长度,避免占用过多内存
#endif
struct _Py_float_state {
#if PyFloat_MAXFREELIST > 0
/* Special free list
free_list is a singly-linked list of available PyFloatObjects,
linked via abuse of their ob_type members. */
int numfree; // 维护空闲链表当前长度
PyFloatObject *free_list; // 指向空闲链表头节点的指针
#endif
};
从注释中可以看出,使用ob_type来连接链表。把ob_type当作next指针来用。空链表如图所示
有了空闲链表之后,可以从链表中取出空闲对象,省去内存的开销
上面提到,float对象销毁时,回放到空闲链表,看看源码Objects/floatobject.c,顺着float_dealloc找到_PyFloat_ExactDealloc
void
_PyFloat_ExactDealloc(PyObject *obj)
{
assert(PyFloat_CheckExact(obj));
PyFloatObject *op = (PyFloatObject *)obj;
#if PyFloat_MAXFREELIST > 0
struct _Py_float_state *state = get_float_state();
#ifdef Py_DEBUG
// float_dealloc() must not be called after _PyFloat_Fini()
assert(state->numfree != -1);
#endif
if (state->numfree >= PyFloat_MAXFREELIST) {
PyObject_Free(op);
return;
}
state->numfree++;
Py_SET_TYPE(op, (PyTypeObject *)state->free_list);
state->free_list = op;
OBJECT_STAT_INC(to_freelist);
#else
PyObject_Free(op);
#endif
}
可以看到,若空闲链表长度达到最大限制,则调用PyObject_Free回收对象内存,否则将对象插到空闲链表头部。
学了float对象创建销毁的底层知识,我们来看个小题目
a = 3.14
b = 3.14
id(a) // 4307610768
id(b) // 4307610800
可以看到和整数不同,为啥相同的数内存地址不同呢?
想必大家都清楚了,由于float对象是不可变,每次创建对象都会申请新的内存地址
题目二:
a = 3.14
id(a) // 4307345968
del a
b = 2.22 // 4307345968
id(b)
我们发现变量b内存地址与已销毁的变量a内存地址是一样的,想必大家也知道原因了。float对象销毁时,并没有立即回收内存,而是缓存在空闲链表,此时3.14这个浮点对象为空闲链表的头节点。当创建2.22这个对象时,空闲链表非空,则取出空闲链表的头节点,修改ob_fval的值为2.22,所以内存地址是一样的。
想要第一时间看到最新文章,可以关注公众号:郝同学的测开日记
转载自:https://juejin.cn/post/7220338123808849981