int 对象,永不溢出的整数

逝者如斯夫,不舍昼夜。

—— 春秋·孔子·《论语》

整数溢出

开始介绍 int 对象前,先考考大家:下面这个 C 程序( test.c )运行后输出什么?是 1000000000000 (一万亿)吗?

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc, char *argv[])
{
    int value = 1000000;
    printf("%d\n", value * value);

    return 0;
}

可能有不少人觉得这没啥好问的,一百万乘以一百万不就是一万亿吗?但现实却不是如此。

在计算机中,由于变量类型存储空间固定,它能表示的数值范围也是有限的。以 int 为例,该类型长度为 32 位,能表示的整数范围为 -2147483648 至 2147483647 。一万亿显然超出该范围,换句话讲程序发生了 整数溢出 。因此,运行 test.c ,程序这样输出也就不奇怪了:

1
2
3
$ gcc -o test test.c
$ ./test
-727379968

不仅是 C 语言,很多编程语言都存在整数溢出的问题,数据库中的整数类型也是。由于整数溢出现象的存在,程序员需要结合业务场景,谨慎选择数据类型。一旦选择不慎或者代码考虑不周,便会导致严重 BUG 。

int 对象的行为

与其他语言相比, Python 中的整数永远不会有溢出的现象。一百万乘以一百万, Python 可以轻易算出来:

1
2
>>> 1000000 * 1000000
1000000000000

Pyhton 甚至可以计算十的一百次方,这在其他语言是不可想象的:

1
2
>>> 10 ** 100
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

计算结果如此庞大,就算用 64 位整数,也难以表示。但 Python 中的整数对象却可以轻松应付,完全不需要任何特殊处理。为什么 Python 整数有这样的魔力呢?让我们深入整数对象源码,拨开心中的迷雾。

在源码中,我们将领略到 C 语言 实现大整数的艺术 。也许你曾经被面试官要求用 C/C++ 实现大整数,却因为考虑不周而不幸败北。不要紧,掌握 Python 整数的设计秘密后,实现大整数对你来说将是易如反掌。

int 对象的设计

int 对象在 Include/longobject.h 头文件中定义:

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

我们顺着注释找到了 Include/longintrepr.h ,实现 int 对象的结构体真正藏身之处:

1
2
3
4
struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

这个结构我们并不陌生,说明 int 对象是一个变长对象。除了变长对象都具有的公共头部,还有一个 digit 数组,整数值应该就存储在这个数组里面。 digit 又是什么呢?同样在 Include/longintrepr.h 头文件,我们找到它的定义:

1
2
3
4
5
6
7
#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit;
// ...
#elif PYLONG_BITS_IN_DIGIT == 15
typedef unsigned short digit;
// ...
#endif

看上去 digit 就是一个 C 语言整数,至此我们知晓 int 对象是通过整数数组来实现大整数的。一个 C 整数类型不够就两个嘛,两个不够那就 n 个!至于整数数组用什么整数类型来实现, Python 提供了两个版本,一个是 32 位的 uint32_t ,一个是 16 位的 unsigned short ,编译 Python 解析器时可以通过宏定义指定选用的版本。

这主要是出于内存方面的考量:对于范围不大的整数,用 16 位整数表示即可,用 32 位就有点浪费。本人却觉得由于整数对象公共头部已经占了 24 字节,省这 2 个字节其实意义不大。

由此可见,选用 16 位整数数组时, int 对象内存增长的粒度更小,有些情况下可以节省 2 个字节。但是这 2 字节相比 24 字节的变长对象公共头部显得微不足道,因此 Python 默认选用 32 位整数数组也就不奇怪了。

PyLongObject

如上图,对于比较大的整数, Python 将其拆成若干部分,保存在 ob_digit 数组中。然而我们注意到在结构体定义中, ob_digit 数组长度却固定为 1 ,这是为什么呢?由于 C 语言中数组长度不是类型信息,我们可以根据实际需要为 ob_digit 数组分配足够的内存,并将其当成长度为 n 的数组操作。这也是 C 语言中一个常用的编程技巧。

通过上面的学习,我们知道 int 对象是通过整数数组来实现大整数的。那么,大整数实现的原理又是如何的呢?点击 阅读原文,获取更多细节!

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注:

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: