你有没有试过在 Python 里算 2 ** 10000?它不会像 C 那样默默变成 0,也不会抛异常,而是老老实实吐出一长串数字。很多人说"Python 的 int 没有上限",这句话对,但它真正的代价藏在内存里。今天我把 CPython 的整数对象拆开,看看它为了"永不溢出"到底付出了什么。
一、先说一个反直觉的小事实
在 Python 启动的那一刻,解释器就偷偷把 -5 到 256 这 262 个整数全部预先建好缓存起来了。你可以验证:
a = 256
b = 256
print(a is b) # True,指向同一个对象
x = 257
y = 257
print(x is y) # 在多数交互环境里是 False,每次都是新对象
这意味着"小整数"几乎不占额外内存,而且比较 is 还能命中同一个对象。这一个细节,是很多人优化热点循环时忽略的第一点——反复创建 257 以上的小整数,其实在不断 malloc 新对象。
二、PyLongObject 到底是什么
CPython 源码里,整数对象的核心是这么个结构(简化):
struct _longobject {
PyObject_VAR_HEAD
int ob_digit[1]; // 变长数组,每一位存 30 个二进制位
};
关键点有三个,我一个一个说:
ob_size同时编码"长度"和"符号"。它是负数表示这个整数是负的,绝对值才是数组长度。一个字段干了两件事,这是 C 里常见的省内存技巧。ob_digit不是按"字节"存的,而是按 30 位(64 位平台上)一块一块存。也就是说,一个digit是 30 个二进制位,而不是 8 个。这样做是为了在 32 位整型里留足余量做乘法进位,避免溢出。- 数组是变长的。小数字只需要 1 个 digit,大数字就不断追加。这就是"永不溢出"的真相——不够就加 digit,直到内存用完。
三、实测:一个整数到底占多少内存
我在一台 Python 3.13(64 位)上跑了 sys.getsizeof,看不同量级整数占用的字节:
| 数值 | 二进制位数 | 估算 digit 数 | 实际 sizeof |
|---|---|---|---|
0 |
0 | 0 | 28 B |
255 |
8 | 1 | 28 B |
2**30 |
31 | 2 | 32 B |
2**60 |
61 | 3 | 36 B |
2**100 |
101 | 4 | 40 B |
2**525 |
525 | 18 | 96 B |
2**1000 |
1001 | 34 | 160 B |
注意看:2**1000 是 1001 个二进制位,理论上 1001/8 ≈ 126 字节就够了,但实际是 160 字节。多出来的部分,一部分是 30 位对齐的浪费(每个 digit 实际占 4 字节即 32 位,有 2 位闲着),另一部分是对象头 PyObject_VAR_HEAD 的固定开销(28 字节起)。
我个人的判断是:这个设计在"小整数"上极度省(28 字节封顶),但在"几百位以上的大整数"上,相对理论最小编码有约 20%~30% 的冗余。对绝大多数业务来说无所谓,但如果你在内存里塞了几百万个大素数,这个冗余就值得换方案了(比如直接用 gmpy2 的 mpz)。
四、和别的语言比一比
| 类型 | 上限 | 溢出行为 | 内存 |
|---|---|---|---|
C long (64位) |
±9.2×10¹⁸ | 静默回绕 | 8 字节固定 |
Java BigInteger |
无限 | 不溢出 | 变长,类似思路 |
Python int |
无限 | 不溢出 | 28B 起,变长 |
C 快但会溢出(还静默),Java 和 Python 都走"变长"路线保证正确,代价就是慢一点、占内存多一点。
五、三个最容易踩的坑
- 别拿
is比较大整数。只有 -5~256 有缓存,超出范围is结果不可靠,老老实实用==。 - 大整数运算慢。每一位都要做 30 位乘法和进位处理,
2**100000的乘法比你想的贵得多。 sys.getsizeof只算对象本身,如果你把大整数放进列表,列表里存的只是指针,整数对象的内存是另算的——这点做内存评估时经常漏。
结语
Python 的 int 不会溢出,不是魔法,是"用内存换正确性"的取舍:一个 30 位的变长数组 + 复用的对象头。小数字几乎白送,大数字按位计费。理解这一点,你就不会再奇怪为什么 2**1000 是 160 字节,也不会再误用 is 去比较整数了。