Python源码解读之六 浮点数

  • Post author:
  • Post category:python




前言

前面的章节都是概括性的描述Python源码中,对象的创建、特性、使用、销毁等,这一章开始我们就要开始分析Python的常见内置类型对象以及对应的实例对象,看看底层具体是如何事项的。

第一个要分析的是浮点数,因为浮点数相比其他类型比较简单,所以我们第一个先拿浮点数开刀!



浮点数的创建与销毁



对象的结构

浮点数的定义在Include/floatobject.h中,结构比较简单:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

结构图如下:

在这里插入图片描述

除了PyObject这个公共的头部信息之外,只有一个额外的

ob_fval

,用于存储具体的值,并且使用的是C中的double。我们以f = 6.6为例,底层结构如下:



整体结构很简单,每个对象在底层都是由结构体表示的,这些结构体中有的成员负责维护对象的元信息,有的成员负责维护具体的值。上图的6.6,首先我们需要一个字段来维护6.6这个值,而这个字段就是ob_fval。所以浮点数的结构非常简单,直接使用一个C的double来维护。

当我们要将两个浮点数相加,方法前面几章已经提过,通过PyFloat_AsDouble,将两个PyFloatObject中的ob_fval抽出来,转成C的double,然后进行相加,最后再把相加的结果创建一个新的PyFloatObject即可。

具体代码如下:

static PyObject *
float_add(PyObject *v, PyObject *w)
{
    double a,b;
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    a = a + b;
    return PyFloat_FromDouble(a);
}

浮点数(float实例对象)的结构我们已经很清晰了,那么我们再来看看float类型对象在底层的结构。与实例对象不同,float类型对象全局为一,底层对应定义好的静态全局变量PyFloat_Type,位置在Objects/floatobject.c中。

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字段保存了类型名称,是一个*

    char


    ,显然值为

    “float”

  • tp_dealloc、tp_init、tp_alloc和tp_new字段是与对象创建销毁相关的函数
  • tp_repr字段对应**

    repr

    **方法,生成语法字符串
  • tp_str字段对应**

    str

    **方法,生成普通字符串
  • tp_as_number字段对应数值对象支持的方法簇
  • tp_hash字段是哈希值生成函数


PyFloat_Type

很重要,作为浮点数的类型对象,它决定了浮点数的生死和行为。



浮点数的创建

下面我们来看看浮点数的创建过程,在前两章中,我们初步了解过创建实例对象的一般过程。对于内置类型的实例对象,可以使用Python/C API创建,也可以通过调用类型对象创建。

调用类型对象float创建实例对象,解释器执行的是类型对象type中的tp_call函数。tp_call中会先调用类型对象(这里是float)的tp_new为其实例对象申请一份空间,申请完毕之后对象就已经创建好了。然后会再调用tp_init,并将实例对象作为参数传递进去,进行初始化,也就是设置属性。

但是对于float来说,它内部的tp_init成员是0,从PyFloat_Type的定义我们就可以看到。这就说明float没有__init__,原因是浮点数是一种很简单的对象,初始化操作只需要一个赋值语句,所以在tp_new中就可以完成。怎么理解这句话呢?我们举个栗子:

class Girl1:
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
# __new__ 负责开辟空间、生成实例对象
# __init__ 负责各实例对象绑定属性

# 但其实__init__所做的工作可以直接在__new__当中完成
# 换言之有 __new__ 就足够了,其实可以没有 __init__
# 我们将上面的例子改写一下

class Girl2:

    def __new__(cls, name, age):
        instance = object.__new__(cls)
        instance.name = name
        instance.age = age
        return instance


g1 = Girl1("奈文摩尔", 16)
g2 = Girl2("奈文摩尔", 16)
print(g1.__dict__ == g2.__dict__)  # True

我们看到效果是等价的,因为__init__里面是负责给 self 绑定属性的,这个 self 是

new

返回的。那么很明显,我们也可以在

new

当中绑定属性,而不需要

init

但是按照规范,属性绑定应该放在

init

中执行。只是对于浮点数而言,由于其结构非常简单,所以底层就没有给float实现__init__,所有都是在__new__当中完成的。

print(float.__init__ is object.__init__)  # True
print(tuple.__init__ is object.__init__)  # True
print(list.__init__ is object.__init__)   # False

所以 float 没有

init

,如果获取的话得到的是 object 的

init

,因为 object 是 float 的基类。同理 tuple 也没有,但 list 是有的。

那么下面就来考察一下PyFloat_Type内部的tp_new成员,看看它是如何创建浮点数的。通过PyFloat_Type的定义我们可以看到,在创建的时候给成员tp_new设置是float_new,那么一切秘密就隐藏在float_new里面。

static PyObject *
float_new_impl(PyTypeObject *type, PyObject *x)
{
    //如果type不是&PyFloat_Type,那么必须是它的子类
    //否则调用float_subtype_new会报错
    //但该条件很少触发,因为创建的是浮点数
    //所以type基本都是&PyFloat_Type
    if (type != &PyFloat_Type)
        return float_subtype_new(type, x);
    //然后检测这个 x 类型,如果它是一个字符串
    //那么就根据字符串创建浮点数,比如float("3.14")
    if (PyUnicode_CheckExact(x))
        return PyFloat_FromString(x);
    //不是字符串,则调用PyNumber_Float
    return PyNumber_Float(x);
}

所以核心就在于PyNumber_Float,该函数位于Python/abstract.c中。

PyObject *
PyNumber_Float(PyObject *o)
{
    //方法簇
    PyNumberMethods *m;
    //传递的是NULL,直接返回错误
    if (o == NULL) {
        return null_error();
    }
    //如果传递过来的本来就是个浮点数
    //那么增加引用计数之后,直接返回
    if (PyFloat_CheckExact(o)) {
        Py_INCREF(o);
        return o;
    }
    //走到这里说明不是浮点数,那么它必须要能够转成浮点数
    //也就是类型对象的内部要有__float__这个魔法方法,即nb_float
    //这里拿到相应的方法簇
    m = o->ob_type->tp_as_number;
    //如果方法簇不为空,并且也实现了nb_float
    if (m && m->nb_float) {
        //那么调用nb_float,转成浮点数
        PyObject *res = m->nb_float(o);
        double val;
        //如果 res 不为空、并且是浮点数,直接返回
        //PyFloat_CheckExact检测一个对象的类型是否是float
        if (!res || PyFloat_CheckExact(res)) {
            return res;
        }
        //走到这里说明__float__返回的对象的类型不是一个float
        //如果不是float,那么float的子类目前也是可以的(会抛警告)
        //PyFloat_Check则检查对象的类型是否是float或者其子类
        if (!PyFloat_Check(res)) {
            //如果连子类也不是,那么就会引发TypeError
            //提示__float__返回的对象类型不是float
            PyErr_Format(PyExc_TypeError,
                         "%.50s.__float__ returned non-float (type %.50s)",
                         o->ob_type->tp_name, res->ob_type->tp_name);
            Py_DECREF(res);
            return NULL;
        }
        //到这里说明,res的类型是float的子类
        //那么获取ob_fval成员的值
        val = PyFloat_AS_DOUBLE(res);
        Py_DECREF(res);
        //构建浮点数,返回它的泛型指针 PyObject *
        return PyFloat_FromDouble(val);
    }
    //如果没有__float__,那么会去找__index__
    //这一点和我们之前说过的__int__类似
    if (m && m->nb_index) {
        PyObject *res = PyNumber_Index(o);
        if (!res) {
            return NULL;
        }
        //__index__返回的必须是整数
        //所以调用的是PyLong_AsDouble,而不是PyFloat_AsDouble
        double val = PyLong_AsDouble(res);
        Py_DECREF(res);
        if (val == -1.0 && PyErr_Occurred()) {
            return NULL;
        }
        //根据val构建PyFloatObject
        return PyFloat_FromDouble(val);
    }
    //如果类型不是float,并且内部也没有__float__和__index__
    //那么检测传递的对象的类型是不是float的子类
    //如果是,证明它的结构和浮点数是一致的
    //直接根据ob_fval构建PyFloatObject
    if (PyFloat_Check(o)) { 
        return PyFloat_FromDouble(PyFloat_AS_DOUBLE(o));
    }
    //走到这里就真的没办法了,解释器实在不知道该咋办了
    //所以只能把它当成字符串来生成浮点数了
    return PyFloat_FromString(o);
}

所以一个 float 调用居然要走这么长的逻辑,当然了,这里面也存在很多快分支,总之解释器为我们考虑了很多。我们用 Python 来演绎一下:

# "3.14" 是个字符串
# 在tp_new里面直接调用PyFloat_FromString之后就返回了
print(float("3.14"))  # 3.14

class PI:
    def __float__(self):
        return 3.1415926

# 传递的参数是一个 PI 类型
# 所以会进入PyNumber_Float逻辑
# 由于对象实现了nb_float,所以会直接调用
print(float(PI()))  # 3.1415926

以上是通过类型对象创建,但我们说这种方式底层实际上也是调用了

Python/C API

PyObject *
PyFloat_FromDouble(double fval);

PyObject *
PyFloat_FromString(PyObject *v);
  • PyFloat_FromDouble:通过 C 的浮点数创建Python浮点数
  • PyFloat_FromString:通过Python字符串创建Python浮点数

如果我们是 f = 6.6 这种方式创建的话,那么解释器在编译的时候就知道这是一个浮点数,会调用 PyFloat_FromDouble 一步到胃,因为在该函数内部会直接根据 6.6 创建底层对应的PyFloatObject。而通过类型对象调用的话则会有一些额外的开销,因为这种方式最终也会调用相关的 Python/C API,但是在调用之前会干一些别的事情,比如类型检测等等。


所以 f = 6.6 比 float(6.6)、float(“6.6”) 都要高效。


到这里并没有结束,浮点数的实际创建过程我们还没见到,因为最终还是调用ython/C API 创建的。那么接下来就以PyFloat_FromDouble函数为例,看看浮点数在底层的创建过程,该函数同样位于Objects/floatobject.c中。

PyObject *
PyFloat_FromDouble(double fval)
{  
    //在介绍引用计数时说过,引用计数为0,那么对象会被销毁
    //但是对象所占的内存则不一定回收,而是会缓存起来
    //所以从下面这行代码我们就看到了
    //创建浮点数对象的时候会优先从缓存池里面获取
    //而缓存池是使用链表实现的,每一个节点就是PyFloatObject实例
    //free_list(指针)指向链表的第一个节点
    PyFloatObject *op = free_list;
    //op不是NULL,说明缓存池中有对象,成功获取
    if (op != NULL) {
        //而一旦获取了,那么要维护free_list
        //要将free_list指向链表中的下一个节点
        //但问题来了,为啥获取下一个节点要通过Py_TYPE
        //Py_TYPE不是一个宏吗?用来获取的对象的ob_type
        //相信你已经猜到了,ob_type充当了链表中的next指针
        free_list = (PyFloatObject *) Py_TYPE(op); 
        /*然后还要将缓存池(链表)的节点个数、
          也就是可以直接使用的浮点数对象的数量减去1*/
        //关于缓存池的具体实现,以及为什么要使用缓存池后续会细说
        //目前先知道Python在分配浮点数对象的时候
        //会先从缓存池里面获取就可以了
        numfree--;
    } else {
        //否则的话,说明缓存池里面已经没有可用对象了
        //那么会调用PyObject_MALLOC申请内存
        //PyObject_MALLOC是基于malloc的一个封装
        op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
        //申请失败的话,证明内存不够了
        if (!op)
            return PyErr_NoMemory();
    }
  
    //走到这里说明内存分配好了,PyFloatObject也创建了
    //但是不是还少了点啥呢?没错,显然内部的成员还没有初始化
    //还是那句话,内置类型的实例对象该分配多少空间,解释器了如指掌
    //因为通过PyFloatObject内部的成员一算就出来了。
    //所以虽然对象创建了,但是ob_refcnt、ob_type、以及ob_fval三个成员还没有被初始化
    //所以还要将其ob_refcnt设置为1(因为对于刚创建的对象来说,内部的引用计数显然为1)
    //并将ob_type设置为指向PyFloat_Type的指针,因为它的类型是float
    //而PyObject_INIT是一个宏,它就是专门用来设置ob_type以及ob_refcnt的
    //我们后面看这个宏的定义就知道了
    (void)PyObject_INIT(op, &PyFloat_Type);
    //将内部的ob_fval成员设置为fval,所以此时三个成员都已经初始化完毕
    op->ob_fval = fval;
    //将其转成PyObject *返回
    return (PyObject *) op;
}


所以整体流程如下:

    1. 为实例对象分配内存空间,空间分配完了对象也就创建了,不过会优先使用缓存池
    1. 初始化实例对象内部的引用计数和类型指针
    1. 初始化ob_fval为指定的double值;

这里不知道你有没有发现一个现象,对于我们自定义的类而言,想要创建实例对象必须要借助于类型对象。

但使用Python/C API创建浮点数,却压根不需要类型对象float,而是直接就创建了。创建完之后再让其ob_type成员指向float,将类型和实例关联起来即可。

而之所以能够这么做的根本原因就在于内置类型的实例对象在底层都是静态定义好的,有多少成员已经写死了,所以创建的时候不需要类型对象。解释器知道创建这样的对象需要分配多少内存,所以会直接创建,创建完之后再对成员进行初始化,比如设置类型。

但是对于我们自定义的类而言,想要创建实例对象就必须要借助于类型对象了。

当然啦,这些内容之前也已经说过了,这里再啰嗦一遍,温故知新。这里我们不妨修改一下解释器源代码,根据输出内容猜猜我干了什么事情。

最后看一下

PyObject_INIT

这个宏,它位于Include/objimpl.h中。

#define PyObject_INIT(op, typeobj) \    
( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) )

这个宏接收两个参数,分别是:实例对象的指针和对应的类型对象的指针,然后

Py_TYPE(op)

表示获取其内部的ob_type,将其设置为typeobj,而typeobj在源码中传入的是

&PyFloat_Type

。至于**_Py_NewReference**,这个宏我们在之前也说过了,它负责将对象的引用计数初始化为 1。



浮点数的销毁

当删除一个变量时,Python会通过宏

Py_DECREF

或者

Py_XDECREF

来减少该变量指向的对象的引用计数;当引用计数为0时,就会调用其类型对象中的tp_dealloc指向的函数来回收该对象。当然啦,解释器依旧为回收对象这个过程提供了一个宏**_Py_Dealloc**,我们之前的文章中也说过了。

#define _Py_Dealloc(op) (                               \
    _Py_INC_TPFREES(op) _Py_COUNT_ALLOCS_COMMA          \
    (*Py_TYPE(op)->tp_dealloc)((PyObject *)(op)))


_Py_Dealloc(op)

会调用op对应的类型对象中的析构函数,同时将op自身作为参数传递进去,表示将op指向的对象回收。

而PyFloat_Type中的

tp_dealloc

成员被初始化为

float_dealloc

,所以析构函数最终执行的是

float_dealloc

,在该函数内部我们会清晰地看到一个浮点数被销毁的全部过程。关于它的源代码,我们会在介绍缓存池的时候细说。


总结一下的话,浮点数对象从创建到销毁整个生命周期所涉及的关键函数、宏、调用关系可以如下图所示:


在这里插入图片描述

我们看到通过类型对象调用的方式来创建实例对象,最终也是要走

Python/C API

的,因此肯定没有直接通过

Python/C API

创建的方式快,因为前者多了几个步骤。

如果是float(3.14),那么最终会调用PyFloat_FromDouble(3.14);如果是float(“3.14”),那么最终会调用PyFloat_FromString(“3.14”)。

所以调用类型对象的时候,会先兜个圈子再去使用Python/C API,肯定没有直接使用Python/C API的效率高,也就是说直接使用f=3.14这种方式是最快的。对于其它对象也是同理,当然大部分情况下我们也都是使用Python/C API来创建的。以列表为例,比起

list()

,我们更习惯使用**[]**



缓存池

我们说浮点数这种对象是经常容易被创建和销毁的,如果每次创建都借助操作系统分配内存、每次销毁都借助操作系统回收内存的话,那效率会低到什么程度,可想而知。

因此Python解释器在操作系统之上封装了一个内存池,在内存管理的时候会详细介绍内存池,目前可以认为内存池就是预先向操作系统申请的一部分内存,专门用于小内存对象的快速创建和销毁,这便是Python的内存池机制。

但浮点数使用的频率很高,我们有时会创建和销毁大量的临时对象,所以如果每一次对象的创建和销毁都伴随着内存相关的操作的话,这个时候即便是有内存池机制,效率也是不高的。

举一个简单的例子:

    >>> area = pi * r ** 2

这个语句首先计算半径

r

的平方,中间结果由一个临时对象来保存,假设是

t

; 然后计算圆周率

pi



t

的乘积,得到最终结果并赋值给变量

area

; 最后,销毁临时对象

t

。 这么简单的语句,都会带有隐藏着一个临时对象的创建以及销毁操作。

当然这里一行代码可能感觉不到啥,假设我们要计算很多很多个半径对应的面积呢?显然需要写for循环,如果循环一万次就意味着要创建和销毁临时对象各一万次。

因此,如果每一次创建对象都需要分配内存,销毁对象时需要回收内存的话,那么大量临时对象的创建和销毁就意味着也要伴随大量的

内存分配以及回收操作

,这显然是无法忍受的,更何况Python本身就已经够慢了。

因此Python在浮点数对象被销毁后,并不急着回收对象所占用的内存,换句话说其实对象还在,只是将该对象放入一个空闲的链表中。

之前我们说对象可以理解为一片内存空间,对象如果被销毁,那么理论上内存空间要归还给操作系统,或者回到内存池中。但Python考虑到效率,并没有真正的销毁对象,而是将对象放入到链表中,占用的内存还在。

后续如果再需要创建新的浮点数对象时,那么从链表中直接取出之前放入的对象(我们认为被回收的对象),然后根据新的浮点数对象重新初始化对应的成员即可,这样就避免了内存分配造成的开销。而这个链表就是我们说的

缓存池

,当然不光浮点数对象有

缓存池

,Python中的很多其它对象也有对应的

缓存池

,比如列表。


而浮点对象的缓存池(链表)同样在 Objects/floatobject.c中定义:

#ifndef PyFloat_MAXFREELIST
#define PyFloat_MAXFREELIST    100  
#endif 
static int numfree = 0;  
static PyFloatObject *free_list = NULL;
  • PyFloat_MAXFREELIST:缓存池(链表)中能容纳的浮点数的最大数量,说白了就是链表的最大长度,这里是100个,因为不可能将所有要销毁的PyFloatObject实例都放入到缓存池中
  • numfree:表示当前缓存池(链表)中已经存在的浮点数的个数, 初始为0
  • free_list: 指向链表头结点的指针, 链表里面存储的都是PyFloatObject, 所以头节点的指针就是**PyFloatObject ***

但是问题来了,如果是通过链表来存储的话,那么对象肯定要有一个指针,来指向下一个对象,但是浮点数对象内部似乎没有这样的指针啊。

是的,因为解释器是使用内部的ob_type来指向下一个对象,本来ob_type指向的应该是PyFloat_Type,但在缓存池中指向的是下一个PyFloatObject。

为了保持简洁,

Python



ob_type

字段当作

next

指针来用,将空闲对象串成链表:

在这里插入图片描述

以上就是浮点数的缓存池,说白了就是一个链表,free_list指向链表的头结点,节点之间通过

ob_type

充当

next

指针。

所以

PyFloat_FromDouble

这个API,我们再来回顾一下:

PyObject *
PyFloat_FromDouble(double fval)
{
    // op是缓存池中第一个PyFloatObject的指针
    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;
}

当op不为NULL时,说明缓存池中有缓存好的对象,于是会将链表的头结点取出来重新分配。但是还要维护free_list,因此要获取下一个节点(PyFloatObject实例),然后让free_list指向它。

在链表中,ob_type被用于指向下一个

PyFloatObject

,换言之ob_type保存的是下一个

PyFloatObject的地址

。不过话虽如此,可它的类型仍是struct _typeobject

,或者说PyTypeObject

,因此在存储的时候,下一个

PyFloatObject


一定是先转成了

PyTypeObject


,之后再交给的ob_type,因为对于指针来说,是可以任意转化的,我们一会在看 float_dealloc 的时候就知道了。

那么同理,这里的

Py_TYPE(op)

在获取下一个对象的指针之后,还要转成PyFloatObject *,然后才能交给free_list保存。如果没有下一个对象了,那么

free_list


就是NULL。在下一次分配的时候,上面的if条件

(op != NULL)

就不会成立,从而走下面的else,使用

PyObject_MALLOC

重新分配内存。

以上就是缓存池在浮点数在的创建过程中起到的作用,也就是

对象创建时,会先从缓存池中获取




既然创建时可以从缓存池获取,那么销毁的时候,肯定要放入到缓存池中。而销毁对象时,会调用类型对象的析构函数tp_dealloc,对于浮点数而言就是float_dealloc,我们看下源码,同样位于Objects/floatobject.c当中:

static void
float_dealloc(PyFloatObject *op)
{
    if (PyFloat_CheckExact(op)) {
        //numfree就是当前缓存池已容纳的PyFloatObject实例数量
        //如果达到了缓存池的最大容量
        if (numfree >= PyFloat_MAXFREELIST)  {
            // 那么调用PyObject_FREE回收对象所占内存
            // 因为缓存池的容量不是无限的,这里是100个
            // 当然我们可以修改解释器源代码改变这一点
            // 另外我们注意这里的PyObject_FREE
            // 我们说Python/C API分为两种
            // 这种格式属于“泛型”API
            PyObject_FREE(op);
            return;
        }
        // 没有到达最大容量
        // 此时不需要销毁对象,而是将其放入缓存池中
        // 然后numfree加1
        numfree++;
        // 我们说free_list指向链表的第一个节点
        // 而这里是获取了op的ob_type,让其等于free_list
        // 说明该对象内部的ob_type指向了链表中的头结点
        // 那么显然该对象就成了链表的新的头结点
        // 因此可以看出,对象在插入链表的时候,采用的头插法
        // 但ob_type的类型是struct_typeobject *
        // 所以交给ob_type保存的时候,还要讲free_list的类型转化一下
        // 而获取的时候,再转成PyFloatObject *
        // 这在上面的PyFloat_FromDouble中我们已经看到
        Py_SET_TYPE(op, (PyTypeObject *)free_list);
        // free_list始终指向链表中的头结点,但现在头结点变了
        // 所以最后再让free_list = op, 指向新添加的PyFloatObject
        // 因为它被插入到了链表的第一个位置上
        free_list = op;
    }
    // 否则的话,说明PyFloat_ChectExact(op)为假
    // PyFloat_ChectExact(op)用于检测op的类型是不是float
    // 为假的话,说明此时的类型不是float
    // 那么通过Py_Type(op)->tp_free直接获取对应的类型对象的tp_free
    // 然后释放掉op指向的对象所占的内存
    else
        Py_TYPE(op)->tp_free((PyObject *)op);
}

这便是Python浮点数缓存池的全部秘密,由于缓存池在提高对象分配效率方面发挥着至关重要的作用,所以Python很多其它的内置实例对象也都实现了缓存池,我们后续在分析的时候会经常看到它的身影。

说白了缓存池的作用只有一个,就是在对象被销毁的时候不释放所占的内存,下次创建新的对象时能够直接拿来用。因为内存没有被释放,因此创建起来就快很多。



浮点数行为


PyFloat_Type

中定义了很多的函数指针,比如:type_repr、tp_str、tp_hash等等,这些函数指针将一起决定浮点数的行为,例如:tp_hash决定了浮点数的哈希值计算:

>>> e = 2.71
>>> hash(e)
1637148536541722626
>>>

tp_hash指向的是float_hash,还是那句话,Python底层的函数命名以及API都是很有规律的,相信你能慢慢发现。

static Py_hash_t
float_hash(PyFloatObject *v)
{  
    //我们看到调用了_Py_HashDouble
    //计算的就是ob_fval成员的哈希值
    return _Py_HashDouble(v->ob_fval);
}



浮点数的运算

由于加减乘除等数值操作很常见,所以Python将其抽象成数值操作簇

PyNumberMethods

,并让内部成员tp_as_number指向。数值操作簇

PyNumberMethods

在头文件

Include/object.h

中定义:

typedef struct {
    binaryfunc nb_add;
    binaryfunc nb_subtract;
    binaryfunc nb_multiply;
    binaryfunc nb_remainder;
    binaryfunc nb_divmod;
    ternaryfunc nb_power;
    unaryfunc nb_negative;
    // ...

    binaryfunc nb_inplace_add;
    binaryfunc nb_inplace_subtract;
    binaryfunc nb_inplace_multiply;
    binaryfunc nb_inplace_remainder;
    ternaryfunc nb_inplace_power;
    //...
} PyNumberMethods;


PyNumberMethods

定义了各种数学算子的处理函数,数值计算最终由这些函数执行,当然这些函数就是魔法方法的底层实现。

处理函数根据参数个数可以分为:

一元函数(unaryfunc)



二元函数(binaryfunc)



三元函数(ternaryfunc)



对于PyFloat_Type而言,在初始化的时候给成员tp_as_number赋的值为**&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 */
    // ...
};

以加法为例,最终执行

float_add

,显然它是一个二元函数,我们看一下底层实现。

static PyObject *
float_add(PyObject *v, PyObject *w)
{  
    //显然两个Python对象相加
    //一定是先将其转成C的对象,然后再相加
    //加完之后再根据结果创建新的Python对象
    //所以声明了两个double
    double a,b;
    //CONVERT_TO_DOUBLE是一个宏,从名字上也能看出来它的作用
    //将PyFloatObject里面的ob_fval抽出来,赋值给double变量
    //这个宏有兴趣可以去源码中看一下,也在当前文件中
    CONVERT_TO_DOUBLE(v, a);  // 将ob_fval赋值给a
    CONVERT_TO_DOUBLE(w, b);  // 将ob_fval赋值给b
    //将a和b相加赋值给a
    a = a + b;
    PyFPE_END_PROTECT(a)
    //根据相加后的结果创建新的PyFloatObject对象
    //当然返回的是泛型指针PyObject *
    return PyFloat_FromDouble(a);
}

因此以上就是浮点数的运算,核心就是:

  1. 定义两个double变量:a、b
  2. 将用来相加的两个浮点数维护的值(ob_fval)抽出来赋值给a和b
  3. 让a和b相加,将相加结果传入PyFloat_FromDouble中创建新的PyFloatObject,然后返回其PyObject *

以上便是浮点数的加法运算,所谓的浮点数在底层就是一个PyFloatObject结构体实例。而两个结构体实例无法相加,所以必须先将结构体中维护的值抽出来,对于浮点数而言就是ob_fval,然后转成C的double再进行相加。最后根据相加的结果创建新的结构体实例,于是新的Python对象便诞生了。

假设 a, b = 1.1, 2.2,那么 c=a+b 的流程就如下所示:

在这里插入图片描述

但如果是C中的两个浮点数相加,那么a + b在编译之后就是一条简单的机器指令,然而Python则需要额外做很多其它工作。

并且在介绍整数的时候,你会发现Python的整数相加会更麻烦,但对于C而言同样是一条简单的机器码就可以搞定。当然啦,因为Python3的整数是不会溢出的,所以需要额外的一些处理,等介绍整数的时候再说吧。

所以这里我们也知道Python为什么会比C慢几十倍了,从一个简单的加法上面就可以看出来。



小结

到此浮点数我们就介绍完了,之所以先介绍浮点数,是因为浮点数最简单。至于整数相比于浮点数会更复杂一点,所以我们这里就先拿浮点数开刀了。

首先我们介绍了浮点数的创建和销毁,创建有两种方式,使用Python/C API更快一些。

销毁的时候则调用类型对象内部的tp_dealloc,浮点数的话就是float_dealloc。当然为了保证效率,避免内存的创建和回收,解释器为浮点数引入了缓存池机制,我们也分析了背后的原理。

最后浮点数还支持相关的数值型操作,PyFloat_Type中的

tp_as_number

指向了

PyNumberMethods

结构体实例

float_as_number

,里面有大量的函数指针,每个指针指向了具体的函数,专门用于浮点数的运算。

当然整型也有,只不过指针指向的函数是用于整数运算的。比如相加:对于浮点数来说,PyNumberMethods结构体成员nb_add指向了函数float_add;对于整数来说,nb_add则是指向了long_add。

然后我们也以相加为例,看了float_add函数的实现,核心就是将Python对象的值抽出来,转成C的类型,然后运算,最后再根据运算的结果,创建Python的对象、并返回泛型指针。

至于减法,乘法,除法其实都是类似的,有兴趣的同学可以在floatobject.c中自行查看。



版权声明:本文为java_0429原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。