int 对象,永不溢出的整数

整数溢出

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

#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 ,程序这样输出也就不奇怪了:

$ gcc -o test test.c
$ ./test
-727379968

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

int 对象的行为

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

>>> 1000000 * 1000000
1000000000000

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

>>> 10 ** 100
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

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

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

int 对象的设计

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

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

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

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

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

#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 解析器时可以通过宏定义指定选用的版本。

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

整数对象

对象大小(16位整数数组)

对象大小(32位整数数组)

1

24 + 2 * 1 = 26

24 + 4 * 1 = 28

1000000

24 + 2 * 2 = 28

24 + 4 * 1 = 28

10000000000

24 + 2 * 3 = 30

24 + 4 * 2 = 32

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

../../_images/8ad6bad6f126e5a133848cfaa6ab4fb7.svg

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

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

更多章节

洞悉 Python 虚拟机运行机制,探索高效程序设计之道!

到底如何才能提升我的 Python 开发水平,向更高一级的岗位迈进? 如果你有这些问题或者疑惑,请订阅我们的专栏,阅读更多章节:

https://cdn.fasionchan.com/python-source-course-qrcode.png

附录

订阅更新,获取更多学习资料,请关注我们的 微信公众号

微信搜索:小菜学编程

创作不易,如果觉得我们写得还行,就请我们喝杯咖啡吧😋

微信搜索:小菜学编程