Python 的 is 为什么时灵时不灵?拆开常量合并、小整数池与 intern 三层机制
几乎每个 Python 教程都会告诉你:「is 比较身份,别用它比较数字,因为只有 -5 到 256 的小整数被驻留」。可如果你真在机器上敲一遍,会看到一个让教程作者尴尬的结果:257 is 257 返回 True。
而 int('257') is int('257') 返回 False。
同一个 257,同一台机器,截然相反。这不是玄学,是三层不同的机制叠在一起造成的。本文用 CPython 3.13.14 实机数据,把「is 时灵时不灵」彻底拆开。
一、先看现场:一组互相矛盾的结果
257 is 257 # True(同一代码块内)
f() is g() # True(f、g 是两个函数,各自 return 257)
exec 单元 A 的 a() is 单元 B 的 b() # False
int('257') is int('257') # False
256+1 is 257 # True(字面量运算,编译期折叠)
a=256; b=1; (a+b) is 257 # False(变量运算,真运行时)
如果「驻留池只到 256」是全部真相,第一行就该是 False。它却是 True。真相远比一句口诀复杂。
二、第一层:编译期常量合并(真正的幕后黑手)
直接看证据——两个函数的常量表:
def f(): return 257
def g(): return 257
f.__code__.co_consts is g.__code__.co_consts # True!
同一个编译单元(一次 compile 调用)里,所有 code object 共享同一个常量表。 编译器编译 f 时把 257 存进常量表,编译 g 时发现表里已有相等的常量,直接复用同一个 PyLong 对象。
所以 257 is 257 为 True,跟驻留池一点关系都没有——这是编译期去重,不是运行期驻留。边界实验同样干净:
ns1 = {}; exec('def a(): return 257', ns1)
ns2 = {}; exec('def b(): return 257', ns2)
ns1['a']() is ns2['b']() # False——两次 exec 是两个编译单元,各自新建
字符串同理:'hello world' is 'hello world' 在同一文件里 True,跨 exec 单元 False。字面量的「is 稳定为真」只在一个编译单元内成立——这也是为什么 .pyc 缓存能让同一文件重复运行结果一致。
三、第二层:小整数池(-5 到 256)
这才是经典口诀说的那层,实测边界:
| 表达式 | 结果 | 机制 |
|---|---|---|
(256+0) is 256 |
True | 折叠成常量 256,池内对象 |
(256+1) is 257 |
True | 折叠成常量 257(同一常量表) |
(-6)+0 is -6 |
True | 同上,且 -6 在池内 |
a=256; b=1; (a+b) is 257 |
False | 变量运算,运行时 BINARY_OP 新建 257 |
最后一行是关键:运算结果超过 256 就是全新对象,哪怕数值相等。第一行的 True 也别得意——那是编译期折叠帮了忙,不是池的功劳。
id 层面的证据更直观:循环 1000 次 id(200) 只有 1 个唯一值(池内对象就一个);1000 个临时大整数 10**6+i 的 id 有 1000 个唯一值(每次新建)。
四、第三层:字符串 intern(比你想的窄)
字符串的驻留分三种情况,实测:
| 场景 | 结果 |
|---|---|
| 同一编译单元的字面量 | True(常量表合并,不是 intern) |
| 单字符字符串跨 loads | True(latin-1 静态字符缓存) |
json.loads 的多字符 key,两次 loads |
False |
sys.intern(s1) is sys.intern(s2) |
True(强制驻留) |
''.join(['a','bc']) is ''.join(['ab','c']) |
False(运行构造) |
json.loads('{"k":1}') 两次解析的 key 'k' 之所以共享,是因为单字符字符串命中静态缓存,不是解析器 memo——换成多字符 'alpha' 立刻露馅。运行期拼接的字符串永远不要假设 is 为真,要共享请显式 sys.intern。
五、is 与 == 分道扬镳的三个路口
| 表达式 | == | is | 说明 |
|---|---|---|---|
[1,2] == [1,2] |
True | False | == 走 __eq__ 协议,is 只比指针 |
自定义 __eq__ 恒返回 True 的对象 == 42 |
True | False | == 可被重载,is 不可 |
True == 1 |
True | False | bool 是 int 子类,值相等但对象不同 |
== 问的是「像不像」,is 问的是「是不是同一个」。重载过 __eq__ 的类,这两个问题的答案可以毫无关系。
六、id() 的陷阱:地址会被回收再利用
id 只是对象在存活期间的地址,不是终身编号:
for _ in range(1000):
tmp = 10**6 + _
ids.add(id(tmp)) # 1000 个唯一 id
1000 个临时大整数拿到 1000 个不同 id——因为每个都活到了 add 那一刻。但循环外再取早就销毁的对象的 id,毫无意义。「id 相同」只在两个 id 都取到的瞬间有效。
七、实战守则
is只用于单例:x is None、x is True/False。None 是单例(None is None恒 True),这是 is 的正确战场- 数字、字符串、列表一律用 ==:不要用
is判断值相等,哪怕它碰巧为真——那是编译期福利,换个写法就失效 - 看懂
257 is 257的 True:是同一编译单元常量合并,不是驻留池扩容,别把这个当定律写进团队规范 - 需要运行期字符串共享(如大量重复 key 的字典),显式
sys.intern,别指望隐式行为 - 真值判断用
is not None:if x:会把 0、''、[] 一锅端,if x is not None:才精确
一句话总结:is 的「时灵时不灵」,是编译期常量合并、小整数池、intern 三套机制在不同代码形态下轮流显形的结果。口诀只说对了最窄的那一层。真正安全的用法只有一条——对单例用 is,对其他一切用 ==。

浙公网安备 33010602011771号