likes
comments
collection
share

Python float对象深度解析

作者站长头像
站长
· 阅读数 5

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 函数对其进行初始化,如图所示

Python float对象深度解析

从上面看到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;
}
  1. 为对象分配内存空间,优先使用空闲对象缓存池
  2. 初始化对象类型字段ob_type以及引用计数字段ob_refcnt
  3. 将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_MAXFREELISTfree_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指针来用。空链表如图所示

Python float对象深度解析

有了空闲链表之后,可以从链表中取出空闲对象,省去内存的开销

上面提到,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,所以内存地址是一样的。

想要第一时间看到最新文章,可以关注公众号:郝同学的测开日记