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 位整数数组也就不奇怪了。
如上图,对于比较大的整数, Python 将其拆成若干部分,保存在 ob_digit 数组中。 然而我们注意到在结构体定义中, ob_digit 数组长度却固定为 1 ,这是为什么呢? 由于 C 语言中数组长度不是类型信息,我们可以根据实际需要为 ob_digit 数组分配足够的内存,并将其当成长度为 n 的数组操作。 这也是 C 语言中一个常用的编程技巧。
通过上面的学习,我们知道 int 对象是通过整数数组来实现大整数的。那么,大整数实现的原理又是如何的呢? 点击 更多章节,获取更多细节!
更多章节¶
洞悉 Python 虚拟机运行机制,探索高效程序设计之道!
到底如何才能提升我的 Python 开发水平,向更高一级的岗位迈进? 如果你有这些问题或者疑惑,请订阅我们的专栏,阅读更多章节: