楔子

本篇文章来聊一聊浮点数支持的操作,之前说过实例对象的相关操作都定义在类型对象里面,所以我们需要查看 PyFloat_Type。

// Objects/floatobject.c
PyTypeObject PyFloat_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "float",
    sizeof(PyFloatObject),
    // 浮点数的 __repr__ 方法
    (reprfunc)float_repr,                       /* tp_repr */
    // 浮点数作为数值对象拥有的算数操作
    &float_as_number,                           /* tp_as_number */
    // ...
    // 浮点数的哈希操作
    (hashfunc)float_hash,                       /* tp_hash */
    // ...
    // 浮点数支持的比较操作
    float_richcompare,                          /* tp_richcompare */
    // ...
};

还是之前说的,Python 底层的函数命名以及 API 都是很有规律的,举个例子:

  • tp_repr 字段表示实例对象的字符串格式化,在 PyFloat_Type 里面它被赋值为 float_repr。
  • tp_hash 字段表示实例对象的哈希操作,在 PyFloat_Type 里面它被赋值为 float_hash。
  • tp_richcompare 字段表示实例对象的比较操作,所有的比较运算均由该字段负责实现,在 PyFloat_Type 里面它被赋值为 float_richcompare。

下面我们来通过源码看一下底层实现。

浮点数的字符串打印

由于 PyFloat_Type 没有实现 tp_str(字段的值为 0 ),所以打印一个浮点数会执行 tp_repr,它对应的具体实现为 float_repr 函数。

// Objects/floatobject.c

static PyObject *
float_repr(PyFloatObject *v)
{
    PyObject *result;  // 返回值
    char *buf;
    // 将 Python 浮点数转成 C 的浮点数,然后再转成字符串
    buf = PyOS_double_to_string(PyFloat_AS_DOUBLE(v),
                                'r', 0,
                                Py_DTSF_ADD_DOT_0,
                                NULL);
    if (!buf)
        return PyErr_NoMemory();
    // 基于 C 字符串创建 Python 字符串
    result = _PyUnicode_FromASCII(buf, strlen(buf));
    // 释放 buf,然后返回
    PyMem_Free(buf);
    return result;
}

比较简单,当然具体的转换逻辑由 PyOS_double_to_string 函数负责,内部最终会调用 C 的库函数,感兴趣可以看一下。

浮点数的哈希值

获取浮点数的哈希值会执行 tp_hash,它对应的具体实现为 float_hash。

// Objects/floatobject.c
static Py_hash_t
float_hash(PyFloatObject *v)
{
    return _Py_HashDouble(v->ob_fval);
}

具体的哈希计算逻辑由 _Py_HashDouble 负责,通过 v->ob_fval 拿到 C 浮点数,然后传进去计算哈希值。

感兴趣可以看一下具体的哈希值计算逻辑,该函数位于 Python/pyhash.c 中。

浮点数的比较操作

浮点数之间的比较操作由 tp_richcompare 字段负责实现,该字段的值为 float_richcompare。

// Include/object.h
#define Py_LT 0  // 小于
#define Py_LE 1  // 小于等于
#define Py_EQ 2  // 等于
#define Py_NE 3  // 不等于
#define Py_GT 4  // 大于
#define Py_GE 5  // 大于等于

// Objects/floatobject.c
static PyObject*
float_richcompare(PyObject *v, PyObject *w, int op)
{   
   // 假设在 Python 里面执行了 3.14 == 2.71
   // 那么这里的 v 和 w 就会指向 3.14 和 2.71,而 op 就是 Py_EQ
    double i, j;
    int r = 0;

    assert(PyFloat_Check(v));
    // 通过 ((PyFloatObject *) v)->ob_fval 拿到具体的 C 浮点数
    i = PyFloat_AS_DOUBLE(v);

    // 变量只是泛型指针 PyObject *,它究竟指向什么类型的对象是需要判断的
    // 对于 v == w 来讲,如果能执行该函数,我们只能确保 v 一定指向浮点数,但 w 则不一定
    // 所以需要判断,如果 w 的类型是 float 或者 float 的子类,那么转成 C double 并赋值给 j
    if (PyFloat_Check(w))
        // 绝大部分情况都会触发此分支
        j = PyFloat_AS_DOUBLE(w);

    else if (!Py_IS_FINITE(i)) {
        // ...
    }

    else if (PyLong_Check(w)) {
        // ...
    } 

    else        /* w isn't float or int */
        goto Unimplemented;

 Compare:
    PyFPE_START_PROTECT("richcompare", return NULL)
    // 拿到 i 和 j 之后,判断 op 是哪一种操作符,然后执行相应的比较逻辑      
    switch (op) {
    case Py_EQ:
        r = i == j;
        break;
    case Py_NE:
        r = i != j;
        break;
    case Py_LE:
        r = i <= j;
        break;
    case Py_GE:
        r = i >= j;
        break;
    case Py_LT:
        r = i < j;
        break;
    case Py_GT:
        r = i > j;
        break;
    }
    PyFPE_END_PROTECT(r)
    return PyBool_FromLong(r);

 Unimplemented:
    Py_RETURN_NOTIMPLEMENTED;
}

该函数的代码量还是有一些大的,但逻辑很好理解,主要是会对 w 做一些类型上的检测。因为 w 不一定是浮点数,比如 3.14 != [] 同样会触发该函数,但函数里的 w 指向的就不是浮点数,而是列表。

不过大部分情况下,两个对象比较的时候,如果符号左侧是浮点数,那么右侧基本也是浮点数。所以基本上都会走开始的 if 分支,然后进入比较逻辑。但如果符号右侧不是浮点数,那么会执行剩下的分支,逻辑会更复杂一些。

浮点数的算数操作

最后是重头戏,来看看浮点数是如何运算的。由于加减乘除等算术操作很常见,所以解释器将其抽象成 PyNumberMethods 方法簇。对于数值型对象来说,它的类型对象会实现此方法簇,并由 tp_as_number 字段指向。

// Include/cpython/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;
    unaryfunc nb_positive;
    unaryfunc nb_absolute;
    inquiry nb_bool;
    unaryfunc nb_invert;
    binaryfunc nb_lshift;
    binaryfunc nb_rshift;
    binaryfunc nb_and;
    binaryfunc nb_xor;
    binaryfunc nb_or;
    // ...
} PyNumberMethods;

PyNumberMethods 这个结构体在前面已经介绍过,每个字段都是一个函数指针,对应一个算术操作。而根据参数个数的不同,这些函数可以分为多种。

  • unaryfunc: 一元函数,只接收一个参数,返回 PyObject *;
  • binaryfunc: 二元函数,接收两个参数,返回 PyObject *;
  • ternaryfunc: 三元函数,接收三个参数,返回 PyObject *;
  • inquiry:一元函数,接收一个参数,但返回的是 int。

它们本质上就是解释器基于参数的类型和个数而起的别名,除了以上这些,还有很多其它的别名,具体可以查看 Include/object.h。

由于浮点数是数值型对象,所以 PyFloat_Type 实现了该方法簇,值为 float_as_number,来看一下,它位于 Objects/floatobject.c 中。

像 float_add 负责浮点数的加法运算,float_sub 负责浮点数的减法运算,都比较简单。但我们看到有的函数指针被赋值成了 0,如果为 0 则表示不支持相应操作,比如浮点数不支持位运算。

在 C 语言中,给指针类型的字段赋值为 0 和赋值为 NULL 是等价的。

好,下面我们以加法运算为例,看一下具体实现。

// Objects/floatobject.c
static PyObject *
float_add(PyObject *v, PyObject *w)
{
    // 显然两个 Python 浮点数相加,一定是先转成 C 的浮点数,然后再相加
    // 加完之后再根据结果创建新的 Python 浮点数
    double a,b;  // 声明两个 double 变量
    // CONVERT_TO_DOUBLE 是一个宏,从名字上也能看出来它的作用
    // 将 PyFloatObject 里面的 ob_fval 抽出来,赋值给 double 变量
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    PyFPE_START_PROTECT("add", return 0)
    // 将 a 和 b 相加,然后再重新赋值给 a      
    a = a + b;
    PyFPE_END_PROTECT(a)
    // 根据相加后的结果创建新的 PyFloatObject 对象
    // 并将其指针转成 PyObject * 之后返回
    return PyFloat_FromDouble(a);
}

以上就是浮点数的加法运算,核心如下:

  • 定义两个 double 变量 a 和 b。
  • 将相加的两个 Python 浮点数维护的值(ob_fval)抽出来,交给 a 和 b。
  • 让 a 和 b 相加,将相加的结果传入 PyFloat_FromDouble 函数中创建新的 PyFloatObject,然后返回其 PyObject *。

另外 float_add 里面还有两个宏我们没有说,分别是:PyFPE_START_PROTECT 和 PyFPE_END_PROTECT,它们是做什么的呢?首先浮点数计算一般都遵循 IEEE-754 标准,如果计算时出现了错误,那么需要将 IEEE-754 异常转换成 Python 异常,而这两个宏就是用来干这件事情的。

所以我们不需要管它,这两个宏定义在 Include/pyfpe.h 中,并且已经在 Python3.9 的时候被移除了。

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

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

但如果是 C 的两个浮点数相加,那么编译之后就是一条简单的机器指令,然而 Python 则需要额外做很多其它工作。并且后续在介绍整数的时候,你会发现 Python 的整数相加更麻烦,但对于 C 而言同样是一条简单的机器码就可以搞定。

所以为什么 Python 会比 C 慢很多倍,从一个简单的加法上面就可以看出来。

以上是浮点数的加法操作,至于减法、乘法、除法等操作也是类似的。

// Objects/floatobject.c
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);
}

static PyObject *
float_sub(PyObject *v, PyObject *w)
{
    double a,b;
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    PyFPE_START_PROTECT("subtract", return 0)
    a = a - b;
    PyFPE_END_PROTECT(a)
    return PyFloat_FromDouble(a);
}

static PyObject *
float_mul(PyObject *v, PyObject *w)
{
    double a,b;
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    PyFPE_START_PROTECT("multiply", return 0)
    a = a * b;
    PyFPE_END_PROTECT(a)
    return PyFloat_FromDouble(a);
}

static PyObject *
float_div(PyObject *v, PyObject *w)
{
    double a,b;
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    if (b == 0.0) {
        PyErr_SetString(PyExc_ZeroDivisionError,
                        "float division by zero");
        return NULL;
    }
    PyFPE_START_PROTECT("divide", return 0)
    a = a / b;
    PyFPE_END_PROTECT(a)
    return PyFloat_FromDouble(a);
}

代码逻辑是类似的,整个过程就是将 Python 浮点数里面的值抽出来,得到 C 浮点数,然后进行运算,再基于运算的结果创建 Python 浮点数,并返回它的泛型指针。

小结

到此浮点数就介绍完了,之所以先介绍浮点数,是因为浮点数最简单。至于整数,其实并没有那么简单,因为它的值在底层是通过数组存储的。而浮点数的值则是用一个 double 类型的字段来维护,会更简单一些,所以我们就先拿浮点数开刀了。

首先我们介绍了浮点数的创建和销毁,创建有两种方式,分别是使用对象的特定类型API 和调用类型对象。前者速度更快,但只适用于内置数据结构,而后者更加通用。

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

最后浮点数还支持数值运算,PyFloat_Type 的 tp_as_number 字段指向了 PyNumberMethods 结构体实例 float_as_number,里面有大量的函数指针,每个指针指向了具体的函数,专门用于浮点数的运算。至于运算的具体逻辑,我们也以加法为例详细介绍了 float_add 函数的实现。核心就是将 Python 对象内部的值抽出来,转成 C 的类型,然后运算,最后再根据运算的结果创建 Python 对象,并返回泛型指针。

关于浮点数,如果你还想知道它的更多内容,可以进入源码中,大肆探索一番。


 

欢迎大家关注我的公众号:古明地觉的编程教室。

如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。