257 is 257 为什么是 True?小整数缓存背后还藏着一个更容易被忽略的机制
2026/9/9 3:24:10 网站建设 项目流程

「Python 进阶之路」系列 Day31

写在前面

模块六写完,从今天开始进入模块七——面试高频题串讲,接下来几篇不再引入全新知识点,而是把前面容易被单独拎出来问、又特别容易"感觉懂了但一深挖就露馅"的话题重新过一遍。第一篇是is/==和小整数缓存、字符串驻留——Day01 讲过基础概念,今天要挖穿一个反直觉的现象:257 is 257为什么会是True,而 257 明明已经超出了 -5~256 的缓存范围。


一、是什么:两个"幕后优化"

Day01 讲过is比较身份(id())、==比较值(__eq__),也提过 -5~256 的小整数缓存。今天重新拎出来讲透——不只是"是什么",而是深挖为什么有的地方is判断"看起来"符合直觉,有的地方又不符合,这背后是两套独立的机制在起作用:

  • 小整数缓存:CPython 对 -5 到 256 之间的整数提前创建好并常驻内存,任何地方用到这个范围内的整数值,都直接复用这些现成对象
  • 字符串驻留(interning):对"看起来像标识符"的字符串(只包含字母、数字、下划线,且不以数字开头),CPython 会尝试复用已有的相同字符串对象,而不是重新创建

二、为什么:省内存 + 加速比较

这两个优化的动机是一致的:整数和字符串在程序里被大量重复使用(变量名、字典 key、小的计数值),如果每次都创建新对象,既浪费内存,也让==比较变慢。驻留之后,==比较可以先做一次廉价的id()比较,命中就直接返回True,不用再逐字符/逐位比较(这只是==实现里的一个内部优化,不代表==变成了is)。

范围和规则的设计也有取舍:小整数缓存选 -5~256 是因为这个区间覆盖了绝大多数业务代码里常见的计数、索引、状态码;字符串驻留只对"标识符样式"的字符串生效,是因为源码里大量重复出现的字符串常量主要就是变量名、属性名、关键字参数名这一类,把所有字符串都无条件驻留反而会增加不必要的内存和维护开销。


三、怎么用

1. 真正隔离出小整数缓存和字符串驻留

这里有个容易被绕进去的坑:如果两个字面量写在同一条语句/同一个函数里,is判断会被"编译器常量折叠"干扰,看不出真正的缓存边界。exec()把两个值放进完全独立的代码对象,才能验证纯粹的缓存/驻留效果:

defisolated_is(code_a,code_b):ns1,ns2={},{}exec(f"v ={code_a}",ns1)exec(f"v ={code_b}",ns2)returnns1["v"]isns2["v"]print(isolated_is("256","256"))# True —— 在缓存范围内print(isolated_is("257","257"))# False —— 超出缓存范围print(isolated_is("'hello'","'hello'"))# True —— 标识符样式,自动驻留print(isolated_is("'hello world'","'hello world'"))# False —— 带空格,不驻留print(isolated_is("'hello!'","'hello!'"))# False —— 带特殊符号,不驻留

结果完全符合预期:256 在缓存范围内是True,257 超出范围是False"hello"这种标识符样式的字符串自动驻留是True,带空格或特殊符号的字符串不驻留是False

2. 关键陷阱:常量折叠和"缓存"是两回事

两个整数字面量比较is

是否在同一个代码对象里

编译器做常量折叠
大概率共享同一个对象

值是否在负5到256之间

小整数缓存
共享同一个对象

各自独立创建
is通常为False

如果不用exec()隔离,直接在同一条语句里写两个超出缓存范围的整数,会得到一个反直觉的结果:

c,d=257,257print(cisd)# True! 257明明超出了-5~256的缓存范围

dis看字节码就能看穿真相:

importdisdefsame_stmt():c,d=257,257returncisd dis.dis(same_stmt)# LOAD_CONST 1 ((257, 257)) ← 编译器把 (257, 257) 直接折叠成了一个常量元组

这里的True根本不是小整数缓存起的作用,而是编译器在编译这个函数时,发现257这个常量在同一个代码对象里出现了两次,做了"常量去重"——两个257在字节码层面从一开始就是同一个对象。这个优化只发生在同一个代码对象(同一个函数/模块)编译期就能确定的常量上,一旦这两个257来自运行时计算(比如从函数参数、input()、数据库查询结果里来),常量折叠完全不起作用,is判断会老老实实地变成False

3. sys.intern():手动驻留

如果确实需要让运行时拼接出来的字符串也享受驻留的好处(比如高频比较同一批字符串、想用is加速比较),可以用sys.intern()手动驻留:

importsys part="hel"z=part+"lo"# 运行时拼接,不会自动驻留y="hello"print(zisy)# Falsez2=sys.intern(part+"lo")print(z2isy)# True —— 手动驻留后和字面量"hello"共享了同一个对象

四、面试追问

Q1:is==的核心区别是什么?

is比较的是身份,本质是比较两个对象的id()==比较的是值,调用的是对象的__eq__方法。判断值相等永远应该用==,判断是不是同一个对象(尤其是判断None/True/False这类单例)用is

Q2:小整数缓存的范围是多少,为什么要跨代码对象验证?

范围是 -5 到 256。如果两个整数字面量写在同一条语句或同一个函数里,编译器的常量折叠会让结果看起来像"缓存生效了",哪怕数值已经超出了缓存范围;必须用exec()之类的手段,把两者放进完全独立的代码对象,才能验证真实的缓存边界。

Q3:什么样的字符串会被自动驻留?

符合标识符格式的字符串——只包含字母、数字、下划线,且不以数字开头,比如"hello"。带空格、标点等特殊字符的字符串(比如"hello world""hello!")默认不会被自动驻留。

Q4:为什么同一条语句里两个超出缓存范围的整数,is判断却是True

这是编译器的常量折叠/常量去重在起作用,不是小整数缓存——编译器发现同一个代码对象里出现了两次相同的字面量常量,会在字节码层面把它们直接合并成同一个常量对象,用dis反汇编能直接看到这个折叠痕迹(比如(257, 257)被编译成一个共享同一个 257 对象的常量元组)。

Q5:这些行为能不能依赖用来做业务判断?

不能。小整数缓存、字符串驻留、常量折叠都是 CPython 的实现细节和性能优化,不是 Python 语言规范承诺的行为,不同版本、不同实现(比如 PyPy)可能表现不一样。判断值相等永远应该用==,不能依赖is在特定场景下"碰巧"表现出来的相等结果。


下一篇预告

Day32 讲全局变量与global/nonlocal——这两个关键字分别解决什么问题,混用嵌套函数时容易踩的坑有哪些。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询