第三章内建对象(1):小试牛刀,解剖浮点对象 float

前言:后续小结将会陆续介绍python内建对象我们将从对象模型中挑出 float 先讲解,因为它是 Python 中最简单的对象之一,“麻雀虽小,五脏俱全”, 拥有对象的全部必要属性!
  • 以 float 对象为起点开启源码之旅,能够快速上手,为研究更复杂的内置对象打好基础,建立信心!
(1)内部结构
  • float 实例对象在 Include/floatobject.h 中定义
// 结构很简单,和上几节了解的一样
typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

定长对象共用的头部,只有一个额外的字段 ob_fval ,存储对象所承载的浮点值

  • 内置类型float对象,与实例对象不同的点是:float 类型对象 全局唯一 ,因此可以作为 全局变量 定义。
// 在 C 文件 Objects/floatobject.c 中,我们找到了代表 float 类型对象的全局变量 PyFloat_Type 
PyTypeObject PyFloat_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "float",
    sizeof(PyFloatObject),
    0,
    (destructor)float_dealloc,                  /* tp_dealloc */
    0,                                          /* tp_print */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_reserved */
    (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 */
    (reprfunc)float_repr,                       /* tp_str */
    PyObject_GenericGetAttr,                    /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   /* 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 */
};

内容很长!

  • PyFloat_Type 中保存了很多关于浮点对象的 元信息(元数据) ,关键字段包括:

    • tp_name 字段保存类型名称,常量 float
    • tp_dealloctp_inittp_alloc 和 tp_new 字段是对象创建销毁相关函数;
    • tp_repr 字段是生成语法字符串表示形式的函数;
    • tp_str 字段是生成普通字符串表示形式的函数;
    • tp_as_number 字段是数值操作集;
    • tp_hash 字段是哈希值生成函数;
  • PyFloat_Type 很重要,作为浮点 类型对象 ,它决定了浮点 实例对象 的 生死和行为 。 接下来,我们以不同小节分别展开讨论

对象的创建:
  • 调用类型对象 float 创建实例对象,Python 执行的是 type 类型对象中的 tp_call 函数。 tp_call 函数进而调用 float 类型对象的 tp_new 函数创建实例对象, 再调用 tp_init 函数对其进行初始化:图片理解 参考上一节中的例子

  • 除了通用的流程, Python 为内置对象实现了对象创建 API ,简化调用,提高效率

PyObject *
PyFloat_FromDouble(double fval);

PyObject *
PyFloat_FromString(PyObject *v);

// PyFloat_FromDouble 函数通过浮点值创建浮点对象;
// PyFloat_FromString 函数通过字符串对象创建浮点对象

这解释了,不管传入 浮点值,还是字符串,float()都可以创建浮点实例对象。

  • 以 PyFloat_FromDouble 为例,特化的对象创建流程如下:
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;
}

// 为对象 分配内存空间 ,优先使用空闲对象缓存池 (第 4-12 行);
// 初始化 对象类型 字段 ob_type 以及 引用计数 字段 ob_refcnt (第 14 行);
// 将 ob_fval 字段初始化为指定 浮点值 (第 15 行);
  • 其中宏PyObject_INIT 在头文件 Include/objimpl.h 中定义:
#define PyObject_INIT(op, typeobj) \
    ( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) )
  • 宏 _Py_NewReference 将对象引用计数初始化为 1 ,在 Include/Object.h 中定义:
#define _Py_NewReference(op) (                          \
    _Py_INC_TPALLOCS(op) _Py_COUNT_ALLOCS_COMMA         \
    _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA               \
    Py_REFCNT(op) = 1)

对象的销毁

  • 注意:当对象不再需要时, Python 通过 Py_DECREF 或者 Py_XDECREF 宏减少引用计数; 当引用计数降为 0 时, Python 通过 _Py_Dealloc 宏回收对象。
  • 而 _Py_Dealloc 宏调用类型对象 PyFloat_Type 中的 tp_dealloc 函数指针
#define _Py_Dealloc(op) (                               \
    _Py_INC_TPFREES(op) _Py_COUNT_ALLOCS_COMMA          \
    (*Py_TYPE(op)->tp_dealloc)((PyObject *)(op)))
下面总结,对象从创建到销毁整个生命周期所涉及的 关键函数、宏及调用关系 如下,非常清晰!

在这里插入图片描述

这些都相当于是对上一节中 对浮点类型举例的扩展与细致总结。

空闲对象缓存池: 解决频繁的对象创建及销毁时的效率问题(提高对象分配效率)

  • 浮点运算背后涉及 大量临时对象创建以及销毁 ,以计算圆周率为例
>>> area = pi * r ** 2
    • (1)首先计算半径 r 的平方,中间结果由一个临时对象来保存,假设是 t
    • (2) 接着计算圆周率 pi 与 t 的乘积,得到最终结果并赋值给变量 area
    • (3)销毁临时对象t
  • 显然:创建对象时需要分配内存,销毁对象时又需要回收内存。 大量临时对象创建销毁 ,意味着 大量内存分配回收操作,这是非常牺牲效率的!

  • 改善: Python 在浮点对象销毁后,并不急于回收内存,而是将对象放入一个 空闲链表!!,后续需要创建浮点对象时,先到空闲链表中取,省去分配内存的开销。

  • 浮点对象的空闲链表同样在 Objects/floatobject.c 中定义:

#ifndef PyFloat_MAXFREELIST
#define PyFloat_MAXFREELIST    100
#endif
static int numfree = 0;
static PyFloatObject *free_list = NULL;
    • free_list 变量,指向空闲链表 头节点 的指针

    • numfree 变量,维护空闲链表 当前长度

    • PyFloat_MAXFREELIST 宏,限制空闲链表的 最大长度 ,避免占用过多内存

(这里是我没有读懂的地方,ob_type作为 next指针)
在这里插入图片描述

  • 由此一来,需要创建浮点对象时,可以从链表中取出空闲对象,省去申请内存的开销! 以 PyFloat_FromDouble 为例:
PyFloatObject *op = free_list;
if (op != NULL) {
    free_list = (PyFloatObject *) Py_TYPE(op);
    numfree--;
} else {
    op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
    // ...
}
    • (1)检查 free_list 是否为空(第 2 行);
    • (2)如果 free_list 非空,取出头节点备用, 并将 numfree 减一(第 3-4 行)
    • (3)如果 free_list 为空,则调用 PyObject_MALLOC 分配内存(第 6 行);

作者温馨的提示:如果对 C 语言链表操作不熟悉,可以结合以下图示加以理解:
在这里插入图片描述

  • 对象销毁时, Python 将其缓存在空闲链表中,以备后用。考察 float_dealloc 函数:
if (numfree >= PyFloat_MAXFREELIST)  {
    PyObject_FREE(op);
    return;
}
numfree++;
Py_TYPE(op) = (struct _typeobject *)free_list;
free_list = op;
    • 第 1-4 行,空闲链表长度达到限制值,调用 PyObject_FREE 回收对象内存;
    • 第 5-7 行,空闲链表长度暂未达到限制,将对象插到空闲链表头部;
这就是 Python 空闲对象缓存池的全部秘密! 由于空闲对象缓存池在 提高对象分配效率 方面发挥着至关重要的作用, 后续研究其他内置对象时,我们还会经常看到它的身影

对象的行为

  • 举个例子:例如 tp_hash 函数决定浮点哈希值的计算
>>> pi = 3.14
>>> hash(pi)
322818021289917443

tp_hash 函数指针指向 float_hash 函数,实现了针对浮点对象的哈希值算法:

static Py_hash_t
float_hash(PyFloatObject *v)
{
    return _Py_HashDouble(v->ob_fval);
}
数值操作集
  • 上节中提到了,对于共性的操作,python将其抽象为 操作集。
  • 回到 Objects/floatobject.c 观察浮点对象数值操作集 float_as_number 是如何初始化的:
static PyNumberMethods float_as_number = {
    float_add,          /* nb_add */
    float_sub,          /* nb_subtract */
    float_mul,          /* nb_multiply */
    float_rem,          /* nb_remainder */
    float_divmod,       /* nb_divmod */
    float_pow,          /* nb_power */
    (unaryfunc)float_neg, /* nb_negative */
    // ...

    0,                  /* nb_inplace_add */
    0,                  /* nb_inplace_subtract */
    0,                  /* nb_inplace_multiply */
    0,                  /* nb_inplace_remainder */
    0,                  /* nb_inplace_power */
    // ...
};
  • 以加法为例,以下语句在 Python 内部最终由 float_add 函数执行:
>>> a = 1.5
>>> b = 1.1
>>> a + b
2.6
  • float_add 是一个 二元函数 ,同样位于 Objects/floatobject.h 中:
static PyObject *
float_add(PyObject *v, PyObject *w)
{
    double a,b;
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    PyFPE_START_PROTECT("add", return 0)
    a = a + b;
    PyFPE_END_PROTECT(a)
    return PyFloat_FromDouble(a);
}
    • 函数实现只有寥寥几步
      • 首先 将两个参数对象转化成浮点值( 5-6 行);
      • 然后 对两个浮点值求和( 8 行);
      • 最后,创建一个新浮点对象保存计算结果并返回( 10 行)

这就是浮点类型的内容。大部分都是在上节中所讲到的内容! 我们巩固和加深理解。

下一节:讲解两个面试题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值