python源码剖析No.3--浮点对象

前面的章节中,我们提到了python中的float对象是定长对象,那其内部又是怎么布局的呢?这节从源码的角度深入探究一下python中的浮点对象


在往下学习之前,有必要在提一下定长对象的头部

1
2
3
4
5
typedef struct _object {
_PyObject_HEAD_EXTRA //一般用不到
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;

其中_PyObject_HEAD_EXTRA

1
2
3
4
/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;

_PyObject_HEAD_EXTRA是个宏定义,这些字段仅在定义宏 Py_TRACE_REFS 时存在。它们被初始化为 NULL 由 PyObject_HEAD_INIT 宏来处理。对于静态分配的对象,这些字段始终保持 NULL。对于动态分配的对象,这两个字段用于将对象链接到堆上的 all 活对象的双向链表中。这可以用于各种调试目的;目前唯一的用途是在设置环境变量 PYTHONDUMPREFS 时打印在运行结束时仍然活动的对象。

这些字段不会由子类型继承。

ob_refcnt引用计数

ob_type指向对象类型的基类,是一个指向结构体_typeobject 的指针,关于结构体_typeobject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* For printing, in format "<module>.<name>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

/* Methods to implement standard operations */

destructor tp_dealloc;
printfunc tp_print;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
or tp_reserved (Python 3) */
reprfunc tp_repr;

/* Method suites for standard classes */

PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;

/* More standard operations (here for binary compatibility) */

hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
getattrofunc tp_getattro;
setattrofunc tp_setattro;

/* Functions to access object as input/output buffer */
PyBufferProcs *tp_as_buffer;

/* Flags to define presence of optional/expanded features */
unsigned long tp_flags;

const char *tp_doc; /* Documentation string */

/* call function for all accessible objects */
traverseproc tp_traverse;

/* delete references to contained objects */
inquiry tp_clear;

/* rich comparisons */
richcmpfunc tp_richcompare;

/* weak reference enabler */
Py_ssize_t tp_weaklistoffset;

/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;

/* Attribute descriptor and subclassing stuff */
struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;
struct _typeobject *tp_base;
PyObject *tp_dict;
descrgetfunc tp_descr_get;
descrsetfunc tp_descr_set;
Py_ssize_t tp_dictoffset;
initproc tp_init;
allocfunc tp_alloc;
newfunc tp_new;
freefunc tp_free; /* Low-level free-memory routine */
inquiry tp_is_gc; /* For PyObject_IS_GC */
PyObject *tp_bases;
PyObject *tp_mro; /* method resolution order */
PyObject *tp_cache;
PyObject *tp_subclasses;
PyObject *tp_weaklist;
destructor tp_del;

/* Type attribute cache version tag. Added in version 2.6 */
unsigned int tp_version_tag;

destructor tp_finalize;

} PyTypeObject;

以上部分是float对象的头部,稍微提一下一些重点字段的含义,因为后续会不断出现雷同的字段,反复加强印象用:

  • PyObject_VAR_HEAD,可以知道所有类型对象的基类PyTypeObject是一个变长对象.
  • tp_name适用于输出打印出对象类型而设置的
  • tp_basicsize, tp_itemsize用于对象的内存分配设置的


float对象

实际上有float类型对象实例化出来的实例对象为PyFloatObject 定义位置/include/floatobject.h

1
2
3
4
5
6
#ifndef Py_LIMITED_API
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
#endif

可以看到相比头部对了个存放浮点数值的ob_fval

那么问题来了,究其背后,float是对象,属于类型对象,又该是什么样子呢?通过前面的学习,我们知道float类型对象影响着float实例对象的内存分配以及可进行的操作,具体又是如何一个过程呢?下面我们展开。


float类型对象

float类型对象PyFloat_Type的定义位置/Objects/floatobject.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 */
};

字段解读

在python中,一切皆对象。类型对象也是对象,逃不开定长头部PyObject 或变长头部 PyVarObject

PyFloat_Type中,看到其使用的了宏PyVarObject_HEAD_INIT(&PyType_Type, 0)初始化对象头部,向光宏定义在/include/object.h中

1
2
3
4
5
6
#define PyObject_HEAD_INIT(type)        \
{ _PyObject_EXTRA_INIT \
1, type },

#define PyVarObject_HEAD_INIT(type, size) \
{ PyObject_HEAD_INIT(type) size },

可以看到,变长对象头部比定长对象头部多了一个字段ob_size:

对于静态分配类型对象,应将其初始化为零。对于动态分配的类型对象,此字段具有特殊的内部含义。

有了初始化宏定义,我们知道了对于PyFloat_Type的头部信息:

PyFloat_Type中还保存着许多关于浮点对象的元信息:(由于比较多,介绍关键字段)

  • tp_name,指向包含类型名称的NUL终止字符串。对于动态分配的类型对象,这应该只是类型名称,并且模块名称显式地存储在类型dict中作为关键字 __module__的值。

  • tp_basicsize & tp_itemsize,分配对象内存需要的字段。

    有两种类型:具有固定长度实例的类型 tp_itemsize 字段为0,具有可变长度实例的类型具有非零 tp_itemsize字段。

    对于具有固定长度实例的类型,所有实例具有相同的大小,在 tp_basicsize 中给出。

    对于具有可变长度实例的类型,实例必须具有 ob_size 字段,实例大小为 tp_basicsize 加N乘以 tp_itemsize,其中N是对象的“长度”。 N的值通常存储在实例的 ob_size 字段中。

  • tp_dealloc,指向实例析构函数的指针。必须定义此函数,除非类型保证其实例永远不会被释放。

    当新的引用计数为零时,析构函数由 Py_DECREF()Py_XDECREF() 宏调用。此时,实例仍然存在,可能在对应的缓冲池,下次直接分配直接调出来。

    1
    2
    3
    4
    5
    6
    7
    8
    f = float('3.14')
    print(id(f))
    del(f)
    g = float('3.14')
    print(id(g))
    #----------output---------#
    140253030159792
    140253030159792 //释放实例对象分,并不是将其资源完全释放掉,而是放进缓冲区
  • tp_as_number,对应对象的数值型操作集

  • tp_as_sequence,对应对象的序列型操作集

  • tp_as_mapping,对应对象的关联型操作集

  • tp_hash,计算对象的hash值方法

  • tp_call,当对象为可调用对象时,不为null,否则为null

  • tp_flags,一些标识位

    • Py_TPFLAGS_HEAPTYPE,当对象是在堆中分配,置该位。
    • Py_TPFLAGS_BASETYPE,该类型可作为另一类型的基本类型,置该位。
    • Py_TPFLAGS_READY,当 PyType_Ready() 完全初始化类型对象时,该位置位。
    • Py_TPFLAGS_READYING,当 PyType_Ready() 处于初始化类型对象的过程中时,该位被置位。
    • Py_TPFLAGS_HAVE_GC,当对象支持垃圾回收时,该位置位。如果设置此位,则必须使用 PyObject_GC_New() 创建实例,并使用 PyObject_GC_Del() 销毁。
  • tp_methods,指向静态 NULL 终止的 PyMethodDef 结构数组的可选指针,声明此类型的常规方法。

  • tp_base,指向继承类型属性的基本类型的可选指针。

  • tp_dict,在调用PyType_Ready之前,该字段通常应该初始化为 NULL;它也可以被初始化为包含该类型的初始属性的字典。

  • tp_init,指向实例初始化函数的可选指针。

  • tp_alloc,指向实例分配函数的可选指针。

  • tp_new,指向实例创建函数的可选指针。

  • tp_free,指向实例释放函数的可选指针。它的签名是 freefunc

介绍完后,我们看回PyFloat_Type

字段名 字段说明
tp_name 类型名称为float
tp_basicsize = sizeof(PyFloatObject),tp_itemsize = 0 鉴于float为定长对象,因此itemsize=0且对象大小为sizeof(PyFloatObject)
tp_dealloc = float_dealloc 对象析构函数为float_dealloc
tp_as_number = &float_as_number float对象支持数值操作集
tp_hash = (hashfunc)float_hash float对象提供计算对象hash值的函数
tp_methods = float_methods 其中存放float类型的常规方法
tp_new = float_new 实例创建函数的可选指针


对象的创建

泛型

float实例对象的创建需要调用相应的可调用对象float,由于类型对象float是可调用对象,因此类型对象float的tp_call字段不为空。当实例对象创建,会调用类型对象的类型(PyType_Type中的tp_call),之所以是PyType_Type是由于PyFloat_Type的ObjectHead宏初始化设置的

1
PyVarObject_HEAD_INIT(&PyType_Type, 0)

PyType_Type.tp_call又会调用PyFloat_Type.tp_new进行内存分配,大小根据PyFloat_Type.ob_seize进行分配,随后调用PyFloat_Type.tp_init完成对象初始化。

回去看PyFloat_Type.tp_init发现其内容为0,即指向PyFloat_Type对象初始化的函数指针为空,这是由于float是一种简单对象,初始化操作只需要一个赋值语句,完成对PyFloatObject.ob_fval的赋值,这是一个很简单的初始化操作,python直接在PyFloat_Type.tp_new中完成了。


特型

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

查阅文档发现对应的两个API,一个使用C语言中的double完成对象创建,一个则是使用字符串对象创建

跟踪PyFloat_FromDouble,看下特型方法创建对象的流程,代码位置/objects/floatobject.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

首先需要搞清楚free_list是定义在同文件中static PyFloatObject *free_list = NULL; 从字面意义上理解,free_list构建了一个空闲对象缓冲池。

使用特型方法PyFloat_FromDouble创建对象会优先使用空闲对象缓冲池。

  • 当空闲对象缓冲池不为空,则取出一个空闲对象进行分配,然后记录当前空闲对象池中对象个数的numfree-1
  • 当空闲对象缓冲池尾空,则调用PyObject_MALLOC分配大小为一个PyFloatObject大小的内存空间
  • 对象所需内存空间分配成功后调用PyObject_INIT进行初始化工作
  • 最后设置浮点对象的数值,将整个对象返回

跟踪PyObject_INIT ,看看初始化都完成了哪些工作,定位到/Objects/object.c

1
2
3
4
5
6
7
8
9
10
PyObject *
PyObject_Init(PyObject *op, PyTypeObject *tp)
{
if (op == NULL)
return PyErr_NoMemory();
/* Any changes should be reflected in PyObject_INIT (objimpl.h) */
Py_TYPE(op) = tp;
_Py_NewReference(op);
return op;
}
  • 第7行位置是调用了宏 #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)完成的是PyObjectob_type设置,依据是传进来的tp对应的类型

  • 然后是第8行位置,/include/object.h中找到相关的宏定义:

    1
    2
    3
    4
    #define _Py_NewReference(op) (                          \
    _Py_INC_TPALLOCS(op) _Py_COUNT_ALLOCS_COMMA \
    _Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
    Py_REFCNT(op) = 1)

    发现是对象引用计数的一些设置

简言之,特型API创建对象先是从空闲对象池中优先分配,若空闲对象池为空,则使用PyObject_MALLOC分配对应的对象需要的空间,随后调用PyObject_INIT完成初始化工作,设置浮点对象的ob_fval.



对象的销毁

我们在PyFloat_Type中注意到有这样一个字段tp_dealloc ,关于这个字段的描述:

指向实例析构函数的指针。必须定义此函数,除非类型保证其实例永远不会被释放(如单例 NoneEllipsis 的情况)。

当新的引用计数为零时,析构函数由 Py_DECREF()Py_XDECREF() 宏调用。此时,实例仍然存在,但没有对它的引用。

不难判断,该字段与对象销毁行为挂钩。这里以PyFloat_Type对象销毁为例,我们看到PyFloat_Type.tp_dealloc字段对应的内容为(destructor)float_dealloc是函数指针,顺藤摸瓜,来到同文件下的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void
float_dealloc(PyFloatObject *op)
{
if (PyFloat_CheckExact(op)) {
if (numfree >= PyFloat_MAXFREELIST) {
PyObject_FREE(op);
return;
}
numfree++;
Py_TYPE(op) = (struct _typeobject *)free_list;
free_list = op;
}
else
Py_TYPE(op)->tp_free((PyObject *)op);
}
  • 第4行位置的 PyFloat_CheckExact 跟踪发现是个宏调用,定义于/include/floatobject.h,定义如下:

    #define PyFloat_CheckExact(op) (Py_TYPE(op) == &PyFloat_Type)做的工作是当前确认要释放的对象是floatobject

  • 5-11行的内容是查看当前floatobject的空闲对象池是否满了,满了则直接释放对象,否则便将记录floatobject的空闲对象数numfree加一,将要释放的floatobject放入空闲对象池free_list

简言之:就是当 Py_DECREF()Py_XDECREF() 宏调用导致对象引用计数为0 ,则有这两个宏调用析构函数完成对对象的回收,具体回收流程则遵循类似上述代码的分析流程。


空闲对象链表

上边提到了这个机制,这里依旧以浮点数对象为例,做一点内容补充

浮点对象运算背后设计大量临时对象的创建与销毁。

1
2
3
4
pi = 3.14
r = 2
area = pi * r ** 2
area

第三行代码的浮点运算在python内部:

  • 先计算半径r的平方,结果存放在python中的一个临时对象中,这里假设临时变量为 t
  • 然后完成 t 与 pi的成绩运算,最终得到的运算结果存放在 area 中
  • 随后对临时变量进行销毁

如此一来,不难想象会因为临时变量的创建与销毁而增加执行时的开销。

创建对象时需要分配内存,销毁对象需要回收内存。当程序内部存在大量的临时变量的创建&销毁,无疑会给程序带来极大的负担,因此python引入空闲对象缓冲池机制。在该机制下,python在对象销毁后,并不急于收回分配出去的资源,而是将当前销毁的对象放入一个空闲链表,后续当创建相同类型对象时直接从中取出,省去新对象的创建与销毁的开销。以浮点对象PyFloatObject为例,它的空闲链表定义位置在/objects/floatobject.c中:

1
2
3
4
5
#ifndef PyFloat_MAXFREELIST
#define PyFloat_MAXFREELIST 100
#endif
static int numfree = 0;
static PyFloatObject *free_list = NULL;
  • PyFloat_MAXFREELIST空闲缓冲链表最大的容量
  • numfree指示当前空闲缓冲链表内的空闲对象个数
  • free_list指向空闲缓冲链表

由于是保存的事空闲对象,并且以链表形式,存在遍历需要。Python在实现该机制中使用了对象中的ob_type字段作为链表节点的next指针使用

当创建了同样是PyFloat_Type的对象时,以PyFloat_FromDouble为例,有下面这样的行为(这里只截取关键部分):

1
2
3
4
5
PyFloatObject *op = free_list;
if (op != NULL) {
free_list = (PyFloatObject *) Py_TYPE(op);
numfree--;
}

销毁时也是类似的:

1
2
3
4
5
6
7
if (numfree >= PyFloat_MAXFREELIST)  {
PyObject_FREE(op);
return;
}
numfree++;
Py_TYPE(op) = (struct _typeobject *)free_list;
free_list = op;

优先将待销毁对象放入空闲对象链表中,若空闲链表已满,则使用PyObject_FREE进行对象释放



对象的行为

同样以PyFLoat_Type为例,PyFLoat_Type中定义了许多字段,其中某些字段是函数指针,例如:tp_hash、tp_repr等,这些函数指针决定了PyFLoat_Type的允许进行的行为。

之前我们提过,类型不同的对象,其行为是存在行为的,这点相信很好理解,例如对于list对象,可以进行append,而float则不行,在者我们还知道了python根据行为的一些特点,进行了操作型的分类:数值型操作tp_as_number、序列型操作tp_as_sequence、关联型操作tp_as_mapping

比如对于浮点类型PyFloat_Type,允许的操作:&float_as_number, /* tp_as_number */,根据具体类型进行tp_as_number特化处理


这里可以讨论稍微泛用一些,由于这些操作是所有对象通用,只是存在启用与否、行为差异也无妨,因此初步锁定相关代码在/Objects/object.h中定义

数值型操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
typedef struct {
/* Number implementations must check *both*
arguments for proper type and implement the necessary conversions
in the slot functions themselves. */

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;
unaryfunc nb_int;
void *nb_reserved; /* the slot formerly known as nb_long */
unaryfunc nb_float;

binaryfunc nb_inplace_add;
binaryfunc nb_inplace_subtract;
binaryfunc nb_inplace_multiply;
binaryfunc nb_inplace_remainder;
ternaryfunc nb_inplace_power;
binaryfunc nb_inplace_lshift;
binaryfunc nb_inplace_rshift;
binaryfunc nb_inplace_and;
binaryfunc nb_inplace_xor;
binaryfunc nb_inplace_or;

binaryfunc nb_floor_divide;
binaryfunc nb_true_divide;
binaryfunc nb_inplace_floor_divide;
binaryfunc nb_inplace_true_divide;

unaryfunc nb_index;

binaryfunc nb_matrix_multiply;
binaryfunc nb_inplace_matrix_multiply;
} PyNumberMethods;

其中泛化的PyNumberMethods 具体到PyFLoat_Type下的 则是根据PyFLoat_Type可进行的选择启用相关字段,不启用为0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 */
float_float, /* nb_positive */
(unaryfunc)float_abs, /* nb_absolute */
(inquiry)float_bool, /* nb_bool */
0, /* nb_invert */
0, /* nb_lshift */
0, /* nb_rshift */
0, /* nb_and */
0, /* nb_xor */
0, /* nb_or */
float___trunc___impl, /* nb_int */
0, /* nb_reserved */
float_float, /* nb_float */
0, /* nb_inplace_add */
0, /* nb_inplace_subtract */
0, /* nb_inplace_multiply */
0, /* nb_inplace_remainder */
0, /* nb_inplace_power */
0, /* nb_inplace_lshift */
0, /* nb_inplace_rshift */
0, /* nb_inplace_and */
0, /* nb_inplace_xor */
0, /* nb_inplace_or */
float_floor_div, /* nb_floor_divide */
float_div, /* nb_true_divide */
0, /* nb_inplace_floor_divide */
0, /* nb_inplace_true_divide */
};

由此,对象的行为是写在相关定义内部的


序列型操作

泛化方式具象到特定对象的过程也是类似上述的过程,不赘述。


关联型操作

泛化方式具象到特定对象的过程也是类似上述的过程,不赘述。


本节相关的精彩面试题

1
2
3
4
pi = 3.14
r = 2
area = pi * r ** 2
area
  • 上述代码是否存在临时变量的创建,怎么理解?
    • 存在临时变量创建,根据优先级,先进行半径r的平方计算,计算结果存放在临时变量tmp中,随后将tmp与pi做乘积运算,结果存放在area中。最后对临时变量进行销毁
  • Python是如何优化这个问题的?
    • python引入了空闲对象链表机制,临时对象tmp销毁后并不直接回收其资源,而是将其放入空闲对象链表(不满的话)越过了对象销毁部分,随后在下一次创建于tmp相同类型对象时直接从空闲对象链表中取出,越过了对象创建部分。
1
2
3
4
print(id(f))
del(f)
g = float('3.14')
print(id(g))
  • 上述代码中变量f、g的地址一致,这是为什么?
    • f销毁时,对象被放入空闲对象链表头,g创建时,由于对象类型和f一致,从对应类型的空闲对象链表中取出使用,取出的正好是刚刚放入的f,因此指向的地址一致。
Author: Victory+
Link: https://cvjark.github.io/2022/05/14/python源码剖析-浮点对象/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.