257 is 257 为什么是 True?小整数缓存背后还藏着一个更容易被忽略的机制

「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() 把两个值放进完全独立的代码对象,才能验证纯粹的缓存/驻留效果:

python 复制代码
def isolated_is(code_a, code_b):
    ns1, ns2 = {}, {}
    exec(f"v = {code_a}", ns1)
    exec(f"v = {code_b}", ns2)
    return ns1["v"] is ns2["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. 关键陷阱:常量折叠和"缓存"是两回事

flowchart TD A[两个整数字面量比较is] --> B{是否在同一个<br/>代码对象里} B -->|是| C[编译器做常量折叠<br/>大概率共享同一个对象] B -->|否| D{值是否在<br/>负5到256之间} D -->|是| E[小整数缓存<br/>共享同一个对象] D -->|否| F[各自独立创建<br/>is通常为False]

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

python 复制代码
c, d = 257, 257
print(c is d)   # True! 257明明超出了-5~256的缓存范围

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

python 复制代码
import dis
def same_stmt():
    c, d = 257, 257
    return c is d

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

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

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

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

python 复制代码
import sys

part = "hel"
z = part + "lo"          # 运行时拼接,不会自动驻留
y = "hello"
print(z is y)              # False

z2 = sys.intern(part + "lo")
print(z2 is y)               # 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------这两个关键字分别解决什么问题,混用嵌套函数时容易踩的坑有哪些。

相关推荐
2603_965148111 小时前
宠物经济崛起:宠物食品用品API选品指南
大数据·服务器·人工智能·python·生活·宠物
Terra.K1 小时前
后端开发阶段性总结
java·开发语言·后端
计算机学姐2 小时前
基于SpringBoot的旅游系统的设计与实现
java·vue.js·spring boot·后端·spring·java-ee·旅游
华研前沿标杆游学2 小时前
走进字节豆包参观游学考察
python
卷心菜的学习路2 小时前
Spring Boot 多数据源落地:AbstractRoutingDataSource + 注解切面(附源码)
java·spring boot·后端
薄雾晚晴2 小时前
大模型Skill暴论:所有Skill终将消亡?最新Skill方法论与模型增强开发实战
前端·后端·架构
MacroZheng2 小时前
面试官皱眉:"你懂 Vibe Coding,那你说superpowers和grill-me怎么选?",我:"小孩才做选择,我全都要!"
java·人工智能·后端
sylviiiiiia2 小时前
leetcode hot100
python·算法·leetcode
千里码aicood2 小时前
flask-基于注意力机制的异常流量检测与自动防御系统
后端·python·flask