Python 开发中整数缓存机制导致的 `is` 误用问题详解
目录
Python 开发中整数缓存机制导致的 is 误用问题详解
在 Python 中,is 是身份运算符,用于判断两个引用是否指向内存中的同一个对象;而 == 是相等性运算符,用于判断两个对象的值是否相等。由于 CPython 对小整数(-5 到 256)实施了对象缓存与复用机制,导致许多初学者误用 is 比较数值时获得了“看起来正确”的结果,却在大整数或跨作用域时遭遇诡异的 False。
本文将彻底揭开整数缓存机制的面纱,剖析 is 与 == 的本质区别,并提供清晰的避坑指南。
一、问题复现:时而相等,时而不等
a = 256
b = 256
print(a is b) # True ← 居然相等
c = 257
d = 257
print(c is d) # False ← 又不等了
同一个 is 运算符,为什么 256 时返回 True,而 257 时返回 False?
二、is 与 == 的本质区别
| 运算符 | 比较内容 | 典型用途 | 是否受缓存影响 |
|---|---|---|---|
== |
值相等 | 判断数值、字符串内容是否相同 | ❌ 不受影响 |
is |
对象身份(内存地址) | 判断单例(如 None、True、False)或自定义对象引用 |
✅ 极易受影响 |
x = [1, 2, 3]
y = [1, 2, 3]
print(x == y) # True,内容相同
print(x is y) # False,两个不同的列表对象
对整数错误使用 is 相当于在问:“这两个变量是不是绑定了同一个整数盒子?” 而这个问题毫无意义,因为整数的值才是有意义的。
三、小整数缓存机制详解(CPython 实现细节)
1. 缓存范围
CPython 解释器在启动时会预先创建并缓存 -5 到 256 范围内的所有整数对象。这个范围是硬编码在 CPython 源码中的(Objects/longobject.c)。
// 摘自 CPython 源码
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
这意味着:
- 当你写
a = 100时,Python 并不会新建一个整数对象,而是直接返回缓存池中的那个100。 - 所有值为 100 的变量都引用同一个对象,因此
a is b返回True。
2. 大整数(257 及以上)的情况
对于 257,它不在缓存范围内。每次执行 c = 257 时,Python 都会在内存中新建一个整数对象。
c = 257
d = 257
print(id(c)) # 例如 140275432607056
print(id(d)) # 例如 140275432607088 (不同地址)
print(c is d) # False
3. 特殊情况:同一行赋值
c = 257; d = 257
print(c is d) # True ?
如果在同一行或同一个代码块中多次出现相同的字面量常量,Python 的窥孔优化器(peephole optimizer)可能会将它们合并为同一个常量对象,导致 is 返回 True。但这纯粹是编译优化,不可依赖。
# 定义在函数中,编译器会将相同的常量合并
def test():
a = 257
b = 257
return a is b
print(test()) # 很可能输出 True
这正是该陷阱最迷惑人的地方——在不同上下文(交互环境、脚本文件、函数内部)下,is 的行为不一致。
四、整数缓存的范围可以修改吗?
在 CPython 中,小整数缓存范围是编译时固定的,无法在运行时修改。但你可以通过查看 id 来验证边界:
# 边界测试
print(id(-5)) # 每次运行地址相同
print(id(-6)) # 通常每次运行地址不同(取决于是否被复用)
注意: 其他 Python 实现(如 PyPy、Jython)可能有不同的整数缓存策略,甚至缓存范围更大。绝对不要依赖任何特定实现的缓存行为。
五、其他类型的缓存与驻留(Interning)
类似机制也存在于短字符串和单例对象中:
- 短字符串:符合标识符规则的字符串(如
"abc")会被自动驻留(intern),is可能返回True,但这同样是实现细节。 None、True、False:它们是单例对象,在整个 Python 进程中有且仅有一个实例。因此必须使用is比较:if result is None: # 正确 pass- 空元组
():CPython 也复用单个空元组对象。
六、正确用法:什么时候用 is?
| 场景 | 使用 is |
使用 == |
|---|---|---|
与单例 None 比较 |
✅ 必须 | ❌ 不推荐(可能被重载 __eq__ 欺骗) |
与布尔值 True/False 比较 |
✅ 可以(单例) | ✅ 也可以(但直接 if x: 更 Pythonic) |
| 判断两个变量是否指向同一个对象 | ✅ 正确用途 | ❌ 无法判断身份 |
| 比较数值、字符串、列表等值 | ❌ 严禁 | ✅ 正确用途 |
记住黄金法则: 想比较值就用 ==,只有当你明确想知道“是不是同一个对象”时才用 is。
七、调试技巧:如何发现 is 误用?
-
使用
id()查看对象地址a = 256; b = 256 print(id(a), id(b)) -
Linter 静态检查
- PyCharm 会对
is与字面量整数的比较给出警告:“Comparison with literal performed withis”。 flake8插件flake8-bugbear会报B015: Pointless comparison. Did you mean to use==?
- PyCharm 会对
-
编写防御性单元测试
def test_value_comparison(): assert 1000 + 2000 == 3000 # 正确 # 错误示范:assert (1000 + 2000) is 3000 # 可能失败
八、最佳实践总结
- 永远用
==比较数值,哪怕是小整数也不要碰运气用is。 - 只有与
None(及部分单例)比较时才用is,这既是规范也是性能优化(None是单例,is比==快)。 - 不要试图利用或依赖整数缓存,它在不同 Python 版本、不同解释器、不同上下文下行为不一致。
- 警惕交互式环境与脚本文件的差异,交互环境每行单独编译,可能影响常量合并。
- 在代码审查中严格检查
is的使用,确保未滥用。
九、结语
整数缓存机制本身是一项优秀的内存优化,但 is 运算符的误用让这一内部实现细节“泄露”到了用户代码中,造成了不易察觉的 Bug。理解 is 判断的是身份而非值,是破解这一迷局的关键。当你下次看到 if x is 5: 时,请毫不犹豫地改为 if x == 5:——你的代码会因此更加健壮、可读且具有跨平台一致性。
更多推荐


所有评论(0)