2.2 Python3 int详解


整数

在C语言中,由于变量类型存储空间固定,它能表示的数值范围也是有限制的。以int数据类型为例,在C语言中长度为32位,可以描述的整数范围是-2147483648~2147483647。所以在C语言中,如果直接用int类型变量描述过大的值,则将发生整数溢出的现象。

不光是在C语言中,很多其他的编程语言,或者是数据库中的整数类型,都存在这种问题。所以使用的过程当中,一定要注意选择数据类型,否则一定会引发bug。

但是在Python语言中,整数是肯定不会溢出的,再庞大的整数也可以正常展示。这里便与Python中int类型的实现有关。

大整数设计

在Python中,int是利用C语言实现了大整数的设计。在文件Include/longobject.h头文件中,int对象的定义如下:

typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */

依据注释,在文件Include/longintrepr.h中,找到了int对象的结构体_longobject:

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

根据之前的章节,可以很容易了解到int是一个变长对象,除了公共头部,还有一个digit数组,数组长度为1。

首先在当前文件中查看digit的定义:

#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit;
// ...
#elif PYLONG_BITS_IN_DIGIT == 15
typedef unsigned short digit;
// ...
#endif

显然,digit是C语言整数,其存在两个不同的版本,根据代码可以发现,该整数可以是32位的uint32_t,也可以是16位的unsigned short,这两种版本在编译Python解释器的时候可以指定。通过默认使用32位。虽然在整数较小的情况下,16位的整数比32位的整数节约了2个字节,但对于整个整数结构体而言,2个字节的大小可以不用太过考虑,毕竟变长的公共头部已经占用了3*8(24)个字节了。

可能大家已经发现了,当前存储整数数值大小的数组ob_digit的长度定义为1,那么如果整数值过大吗?难道不是跟C语言一样,会溢出吗?这里就涉及C语言动态数组了,在Python中,整数是不可变对象,它的内存空间在整数对象创建时,是根据赋值一次性分配好的,那么C就可以依据需要为数组分配足够的内存。而数组长度定义为1,则是C语言通常将可变的数组的长度申明为1。

了解了int的结构体,看一下具体整数是如何存储的。整数的存储以正数、负数和零作为划分,三者存储的方式有所区别。

  • 正数:值按照大小分为若干份,存储于ob_digit数组中;ob_size存储数组的长度,也是正数;
  • 负数:以绝对值的方式存储于ob_digit数组中;ob_size同样存储数组长度,但采用负数的方式;
  • 零:ob_size为0,ob_digit数组为空;

需要注意的是,如果选取32位的情况下,Python只用ob_digit数组正数的后30位;16位则用后15位。这里与加法进位有关,需要保留位数保证加法不溢出。

以下是几个常见的例子:

前面的0、10、-10都很好理解,后面数据的绝对值计算方式分别是2**30*1 + 2**0*0(二进制1 000000000000000000000000000000)和2**30*4+2**0*1(二进制:100 000000000000000000000000000001)。

到此,我们了解了Python整数对象的设计,即通过串联多个C整数类型。实现Python的大整数能力。

小整数静态对象池

与浮点数一样,整数亦是不可变对象。这就导致了跟浮点数一样的问题,在运算或者循环的过程中,势必涉及到大量的整数对象的创建销毁,从而导致了大量的内存申请与销毁的流程。

对此,Python采用了不同于浮点对象缓存池的策略:小整数对象池。该策略预先将常用的整数对象创建好以备使用。其关键代码在文件Objects/longobject.c中:

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
  • NSMALLPOSINTS宏规定了静态对象池中的非负整数的个数,默认是257个,也就是从0到256;
  • NSMALLNEGINTS宏规定了静态对象池中负整数的个数,默认是5个,也就是从-1到-5;
  • small_ints则是被定义的一个整数对象数组,用来保存预先创建好的小整数对象。

那么,在Python中,小整数静态对象池中默认的整数为-5~256,一共262个整数。而之所以预存这些整数,一方面是根据以往的结论,这262个整数的使用频率很高,另一方面,这些小整数的内存开销相对可控,不会占用太多的内存。

这里强调一下,整数的静态对象池机制某种程度上要比浮点对象缓存池要好,因为可以直接拿来用,而不紧紧是省去了内存分配的流程。其次,浮点数为什么不能预先缓存?那是因为浮点数没有常用不常用之说,预存的意义不大。