← 返回题目列表

为什么 Python 里 a = 256; b = 256 时 a is b 是 True,但 257 就是 False?

中等 第 16 / 22 题 更新于 2026/07/28
Python小整数缓存字符串驻留is

简化版

**因为 CPython 对「小整数」和「部分字符串」做了缓存/驻留(interning)复用同一个对象,导致这些值的 is(比较对象身份)意外返回 True——这是实现优化,不是语言语义。**具体:① 小整数缓存——CPython 在解释器启动时预先创建了 -5256 这 262 个整数对象并缓存,任何地方用到这个范围内的整数都复用同一个对象,所以 a = 256; b = 256a is bTrue;而 257 超出缓存范围、每次是新对象,a = 257; b = 257a is bFalse。② 字符串驻留(interning)——像标识符一样的短字符串(只含字母数字下划线的字面量)会被驻留复用,a = "hello"; b = "hello" 通常 a is bTrue,但含空格/特殊字符的(如 "hello world")一般不驻留。关键结论这是 CPython 的实现细节,不能依赖!判断值相等永远用 ==is 只用于判断 None/单例。核心记忆:小整数 -5~256 被缓存、类标识符的短字符串被驻留,所以它们 is 为真;但这是优化不是语义,比较值一律用 ==

详细版

两个缓存机制

机制范围效果
小整数缓存-5 ~ 256该范围内整数复用同一对象,is 为 True
字符串驻留类标识符的短字符串字面量驻留的字符串复用,is 为 True
# 小整数缓存
a = 256; b = 256
print(a is b)   # True(256 在缓存范围内,同一对象)
a = 257; b = 257
print(a is b)   # False(超出缓存,各是新对象)
a = -5; b = -5
print(a is b)   # True(-5 在范围内)
a = -6; b = -6
print(a is b)   # False(-6 超出下界)

# 字符串驻留
s1 = "hello"; s2 = "hello"
print(s1 is s2)          # True(像标识符,被驻留)
s3 = "hello world"; s4 = "hello world"
print(s3 is s4)          # False(含空格,一般不驻留)

# 都用 == 比较值,永远正确
print(257 == 257)        # True
print("hello world" == "hello world")  # True

⚠️ 这道题的核心不是「记住 256 这个数字」,而是理解「is 比较的是对象身份(内存中是不是同一个对象),而缓存/驻留只是 CPython 为了省内存和加速做的实现优化——它让某些值恰好复用了对象,于是 is 意外为真,但你绝不能依赖这个行为」。为什么缓存小整数?因为 -5~256 这些小数字在程序里用得极其频繁(循环计数、索引、布尔运算……),每次都新建对象太浪费,预先建好复用能显著省内存、省创建开销。为什么驻留标识符字符串?因为 Python 内部大量用字符串做变量名、属性名、字典键,驻留后比较字符串可以先比身份(is,O(1))再比内容,加速字典查找。但这些都是优化,Python 语言层面只保证「== 比较值」——同样的代码在 PyPy、Jython 或不同 CPython 版本上缓存行为可能不同,所以判断值相等永远用 ==is 只留给 NoneTrueFalse 这些单例

完整版教学

一、is 与 == 的本质区别

先把地基打牢——is== 比的是两件事:

== (值相等):
  比较两个对象的"值"是否相等
  底层调用 __eq__ 方法
  a == b → a.__eq__(b)

is (身份相同):
  比较两个变量是否指向"同一个对象"
  底层比较 id(a) == id(b)(对象在内存的地址/身份)
  → 是不是同一个对象,而不是值是否相等

关键:
  值相等(==)→ 不一定是同一个对象
  是同一个对象(is)→ 值一定相等
  → is 是比 == 更强的条件

例子:
  a = [1,2]; b = [1,2]
  a == b  → True (值相等)
  a is b  → False(两个不同的列表对象)
  id(a)   → 140234...(不同地址)
  id(b)   → 140235...

所以 ==比值(调 __eq__)、is 比身份(比 id 是不是同一对象)

is== 比的是两件不同的事:== 比较「值」是否相等(底层调 __eq__)、is 比较是否「同一个对象」(底层比 id,即对象在内存的身份)。关键关系:值相等不一定是同一对象、是同一对象则值一定相等is 是比 == 更强的条件)。理解「==比值(调 eq)、is 比身份(比 id 是不是同一对象);值相等不一定同对象、同对象一定值相等」,就理解了这道题的地基——缓存机制之所以能影响 is,正是因为它让不同变量指向了同一个对象。

二、小整数缓存(-5 ~ 256)

理解小整数缓存的机制:

CPython 的小整数缓存:
  解释器"启动时"就预先创建了 -5 到 256 这 262 个整数对象
  存在一个数组里(small_ints),全程复用
  → 任何地方用到 -5~256 的整数,都返回缓存里的同一个对象

范围:[-5, 256](含两端)
  262 个对象(-5,-4,...,0,...,255,256)

效果:
  a = 100; b = 100 → 都指向缓存的 100 对象 → a is b 为 True
  a = 256; b = 256 → True(256 是上界,在范围内)
  a = 257; b = 257 → False(257 超范围,各自新建)
  a = -5;  b = -5  → True
  a = -6;  b = -6  → False(-6 超下界)

为什么缓存这个范围:
  小整数在程序中用得极其频繁:
    循环计数(for i in range)、索引、+1/-1、比较...
  每次新建整数对象浪费内存和 CPU
  → 预建 262 个复用,显著省开销
  上界 256 = 一个字节的取值 + 常见小数,是经验权衡

注意:这是 CPython 实现细节
  范围(-5~256)是 CPython 定的,其他实现(PyPy)可能不同
  → 不能依赖具体数字

所以小整数缓存:-5~256 预建复用,该范围 is 为真,是省开销的优化

小整数缓存:CPython 在解释器启动时预先创建了 -5256 这 262 个整数对象并缓存(存在 small_ints 数组),全程复用——任何地方用到这范围的整数都返回同一个缓存对象。效果:a=256;b=256 为 True(256 是上界在范围内)、a=257;b=257 为 False(超范围各自新建)、a=-5 为 True、a=-6 为 False(超下界)。为什么缓存这范围:小整数用得极频繁(循环/索引/+1)、每次新建浪费、预建 262 个复用省开销(上界 256 是一个字节取值的经验权衡)。理解「小整数缓存 -5~256 启动时预建复用,该范围 is 为真、超出为假;因小整数用得极频繁预建省开销;是 CPython 实现细节范围可能变」,就掌握了小整数缓存。

三、字符串驻留(interning)

理解字符串驻留机制:

字符串驻留(string interning):
  某些字符串会被"驻留"——存到一个内部池里,相同内容复用同一对象
  → 被驻留的字符串,is 比较为真

哪些字符串会被自动驻留(CPython 经验规则,非语言保证):
  ① 编译期的字符串字面量,且"看起来像标识符"
     (只含字母、数字、下划线,且不以数字开头)
     "hello"、"user_id"、"abc123" → 驻留
  ② 空字符串、单字符字符串
  ③ 编译期常量折叠的结果

哪些一般不驻留:
  ① 含空格/特殊字符的("hello world"、"a-b")
  ② 运行时动态拼接产生的(s = "hel" + input())
  ③ 长字符串

例子:
  "hello" is "hello"       → True(像标识符,驻留)
  "hi there" is "hi there" → False(含空格,不驻留)
  a = "hello"; b = "".join(["h","e","l","l","o"])
  a is b  → False(b 是运行时拼的,未驻留)

手动驻留:
  import sys
  sys.intern("hi there")  → 强制驻留(返回驻留后的对象)
  → 用于大量重复字符串比较的场景(加速)

为什么驻留:
  Python 内部大量用字符串做变量名/属性名/字典键
  驻留后比较可先比身份(is,O(1))再比内容 → 加速字典查找

所以字符串驻留:像标识符的短字面量被驻留复用,is 为真;可 sys.intern 手动驻留

字符串驻留(interning):某些字符串被存到内部池、相同内容复用同一对象,被驻留的 is 为真。自动驻留的(CPython 经验规则)① 编译期字面量且「看起来像标识符」(只含字母数字下划线、不以数字开头)、② 空/单字符、③ 编译期常量折叠结果一般不驻留的含空格/特殊字符、运行时动态拼接的、长字符串。例子:"hello" is "hello" 为 True、"hi there" is "hi there" 为 False、运行时 join 拼的未驻留为 False。手动驻留sys.intern("...") 强制驻留(大量重复字符串比较场景加速)。为什么驻留:Python 大量用字符串做变量名/属性名/字典键、驻留后可先比身份(O(1))再比内容加速字典查找。理解「字符串驻留:像标识符的短字面量自动驻留复用(is 真)、含空格/运行时拼接不驻留;sys.intern 手动驻留;因加速字典查找(先比身份再比内容)」,就掌握了字符串驻留。

四、为什么 257 就 False 了——现场推演

用一个具体推演把机制串起来:

现场推演:a = 257; b = 257; a is b → ?

① a = 257
   257 不在小整数缓存范围(-5~256)
   → CPython 新建一个整数对象存 257,a 指向它
   → id(a) = 假设 0x1000

② b = 257
   同样 257 不在缓存范围
   → 又新建一个整数对象存 257,b 指向它
   → id(b) = 假设 0x2000(不同地址)

③ a is b
   → id(a)(0x1000) == id(b)(0x2000)? → 否 → False
   (虽然 a == b 是 True,值都是 257)

对比 a = 256; b = 256:
   256 在缓存范围 → a、b 都指向缓存里同一个 256 对象
   → id(a) == id(b) → True

⚠️ 一个易被"骗"的现象——同一行/同一代码块:
   a = 257; b = 257 写在同一行/同一函数里
   CPython 编译时可能把同一个常量 257 折叠复用
   → 这时 a is b 可能是 True(编译期优化,非缓存)
   在交互式解释器逐行输入则各自新建 → False
   → 更说明"别依赖 is 的结果"

所以 257 超缓存范围各自新建对象,id 不同,is 为 False(值 ==仍为 True)

现场推演 a=257;b=257a=257 超出缓存范围(-5~256)→ 新建对象、a 指向它;② b=257 同样超范围 → 又新建对象、b 指向它(不同地址);③ a is b → id 不同 → False(但 a==b 为 True,值都是 257)。对比 256 在范围内 → a、b 都指向缓存同一对象 → True。一个易被「骗」的现象a=257;b=257 写在同一行/同一函数里,CPython 编译时可能把同一常量 257 折叠复用 → a is b 可能是 True(编译期常量折叠,非小整数缓存);交互式逐行输入则各自新建 → False——更说明「别依赖 is 的结果」。理解「257 超范围各自新建 id 不同 is 为 False(==仍 True);同一代码块可能因常量折叠复用导致 is 为 True(非缓存);别依赖 is」,就理解了 257 的现象。

五、正确用法——什么时候用 is、什么时候用 ==

理解正确的使用原则:

使用原则(记死):
  比较"值是否相等" → 永远用 ==
  比较"是不是同一个对象/单例" → 用 is

is 的正确用途(就这几个):
  x is None       ← 判断 None(最常见、最推荐)
  x is True       ← 判断是否就是 True 单例(少用)
  x is False
  x is 某单例对象  ← 如自定义的哨兵 sentinel

为什么判断 None 用 is 而不是 ==:
  ① None 是单例,全局唯一 → is 判断准确、快
  ② == 可能被重写:某对象的 __eq__ 可能让 x == None 意外为 True
     用 is 不受 __eq__ 影响,最可靠
  ③ is 更快(比身份,不调用方法)

绝不要用 is 比较值:
  ✗ if x is 256   (依赖缓存,257 就挂)
  ✗ if name is "admin"  (依赖驻留,动态字符串就挂)
  ✓ if x == 256
  ✓ if name == "admin"

哨兵模式(is 的高级用法):
  _MISSING = object()  # 唯一哨兵
  def f(x=_MISSING):
      if x is _MISSING:  # 用 is 判断"没传参"(区别于传了 None)
          ...

所以比值用==、判 None/单例用 is;绝不用 is 比整数/字符串的值

使用原则(记死)比较值是否相等永远用 ==、比较是不是同一对象/单例用 isis 的正确用途就这几个:x is None(最常见推荐)、x is True/False(少用)、x is 某单例哨兵。为什么判断 None 用 is① None 是单例全局唯一、② == 可能被 __eq__ 重写导致意外为真、is 不受影响最可靠、③ is 更快(比身份不调方法)绝不用 is 比值if x is 256(依赖缓存、257 就挂)、if name is "admin"(依赖驻留、动态字符串就挂)都是错的,要用 ==哨兵模式(is 高级用法):_MISSING = object() 做唯一哨兵、if x is _MISSING 判断「没传参」(区别于传了 None)。理解「比值用==、判 None/单例用 is;None 用 is 因单例+不受__eq__影响+快;绝不用 is 比整数/字符串值;哨兵 object()+is 判断没传参」,就掌握了正确用法。

六、其他实现与版本差异

理解这是实现细节、不同环境可能不同:

这些都是 CPython 的实现细节:
  ① 小整数缓存范围(-5~256)是 CPython 定的
  ② 字符串驻留规则是 CPython 的经验策略
  → 不是 Python 语言规范的一部分

不同实现/版本可能不同:
  ① CPython 不同版本:缓存范围一般稳定(-5~256),
     但字符串驻留、常量折叠的细节可能微调
  ② PyPy:JIT 实现,对象缓存策略不同
  ③ Jython/IronPython:跑在 JVM/.NET 上,行为可能不同

结论——为什么强调"不能依赖":
  同样的 a is b 代码:
    CPython 3.x 交互式:257 → False
    某段被常量折叠的代码:257 → True
    PyPy:可能又不一样
  → is 的结果随实现/上下文变化,依赖它 = 埋 bug

工程建议:
  ① 值比较统一用 ==(跨实现一致、语义正确)
  ② is 只用于 None/单例/哨兵
  ③ 需要驻留优化时显式 sys.intern(别指望自动)
  ④ linter(如 pylint)会警告"用 is 比较字面量"

所以缓存/驻留是 CPython 实现细节、跨实现可能不同,值比较统一用==

这些都是 CPython 的实现细节(小整数范围 -5~256、字符串驻留规则),不是 Python 语言规范。不同实现/版本可能不同:CPython 不同版本缓存范围一般稳定但驻留/常量折叠细节可能微调、PyPy 缓存策略不同、Jython/IronPython 跑在 JVM/.NET 行为可能不同。为什么强调「不能依赖」:同样 a is b 在交互式/常量折叠代码/PyPy 上结果可能不同、依赖它就是埋 bug。工程建议:① 值比较统一用 ==、② is 只用于 None/单例/哨兵、③ 需驻留优化显式 sys.intern、④ linter 会警告用 is 比较字面量。理解「缓存/驻留是 CPython 实现细节非语言规范、跨实现/版本可能不同;is 结果随实现和上下文变化依赖=埋 bug;值比较统一用==、is 只给 None/单例、需驻留用 sys.intern」,就掌握了实现差异。

记忆钩子:「a is b 对 256 为 True、257 为 False,根源是 CPython 的两个优化:①小整数缓存——解释器启动时预建-5~256 这 262 个整数对象全程复用,该范围 is 为真、超出各自新建为假(257 超界所以 False,但同一代码块可能因常量折叠复用又变 True);②字符串驻留——像标识符的短字面量(只含字母数字下划线)自动驻留复用,含空格/运行时拼接的不驻留,sys.intern 可手动驻留;★核心:is 比对象身份(id)、==比值(eq),缓存/驻留只是让某些值恰好复用同一对象导致 is 意外为真,这是实现细节不是语义,跨版本/实现(PyPy)可能不同,绝不能依赖;规则:比值永远用==,is 只留给 None/True/False/哨兵(object())——判 None 用 is 因它是单例且不受__eq__重写影响」

七、常见误区与追问

  • 误区:小整数缓存的范围是 0 到 256。 是 -5 到 256(含两端,共 262 个)——下界是 -5 不是 0;所以 -5 is -5 为 True、-6 is -6 为 False;不过这个具体数字是 CPython 的实现细节、不该背下来当依据,重点是理解「有个小范围被缓存复用、导致 is 意外为真」,真正的规则永远是「比值用 ==」。
  • 误区:字符串只要内容相同 is 就是 True。 不是——只有「看起来像标识符」的短字符串字面量(只含字母数字下划线)才会被自动驻留复用,含空格或特殊字符的(如 “hello world”)一般不驻留、运行时动态拼接产生的字符串也不驻留;所以 "a b" is "a b" 往往是 False;判断字符串相等永远用 ==,需要驻留加速时显式调 sys.intern。
  • 误区:is 比 == 更快更好,能用就用 is。 is 更快是因为它只比身份(id)不调方法,但它比的是「是不是同一个对象」而不是「值是否相等」——用 is 比较值会依赖缓存/驻留这些不可靠的实现细节,257、动态字符串就会挂;is 只该用于 None、True/False、自定义哨兵这些单例判断,值比较必须用 ==。
  • 误区:a = 257; b = 257 一定是 False。 不一定——如果这两行在同一个函数体/同一个编译单元里,CPython 在编译时可能对同一个常量 257 做「常量折叠」复用同一个对象,导致 a is b 为 True;而在交互式解释器里逐行输入则各自新建、为 False;这恰恰说明 is 的结果会随上下文变化、不能依赖。
  • 追问:为什么判断 None 要用 is None 而不是 == None 三个原因:① None 是全局唯一的单例对象,用 is 判断身份最准确;② == 会调用对象的 __eq__ 方法,而某些对象可能重写了 __eq__ 使得 x == None 意外返回 True(比如某些代理对象、numpy 数组的 == 还会返回数组而非布尔值),用 is 不受 __eq__ 影响、最可靠;③ is 只比身份、不调用方法,更快;所以 PEP 8 也规定与 None 比较要用 is/is not。
  • 追问:字符串驻留(interning)有什么实际用途? 主要用途是「加速比较和节省内存」:当程序里有大量重复的字符串(比如解析日志、处理大量记录时反复出现的字段名、类别标签),把它们驻留后,相同内容只存一份(省内存),且比较时可以先用 is 比身份(O(1),如果是同一驻留对象直接判定相等)再退回逐字符比较,加速字典查找和相等判断;Python 内部对变量名、属性名、字典键就用了驻留来加速;需要时可以用 sys.intern(s) 手动驻留一个字符串。
  • 追问:怎么正确判断两个变量是否指向同一个对象、以及看它们的身份?is(或 a is b)判断是不是同一个对象,用 id(obj) 查看对象的身份(CPython 里是内存地址),id(a) == id(b) 等价于 a is b;注意 id 只在对象存活期间唯一,对象被回收后 id 可能被复用;调试时可以用 id() 观察缓存/驻留行为(如 id(256) 在多处相同、id(257) 可能不同),但生产代码不要依赖这些身份来做值判断。

八、加强记忆

a is b 对 256 为 True、对 257 为 False,根源是 CPython 的两个实现优化① 小整数缓存——解释器启动时预先创建了 -5256(共 262 个)整数对象并全程复用,这范围内的整数 is 为真、超出范围各自新建为假(所以 257 是 False;但写在同一代码块里可能因常量折叠复用又变 True);② 字符串驻留(interning)——「看起来像标识符」的短字符串字面量(只含字母数字下划线)会被自动驻留复用(is 为真),含空格/特殊字符或运行时动态拼接的一般不驻留,可用 sys.intern() 手动驻留。核心is 比较对象身份(id)、== 比较值(__eq__),缓存/驻留只是让某些值恰好复用了同一对象、导致 is 意外为真——这是实现细节不是语言语义,跨版本/实现(PyPy 等)可能不同,绝不能依赖规则:比较值永远用 ==is 只留给 NoneTrueFalse 和自定义哨兵(object())——判断 None 用 is None 是因为它是单例且不受 __eq__ 重写影响、更可靠更快。一句话「256 is 为真、257 为假源于 CPython 小整数缓存(-5~256 复用)与字符串驻留(标识符状短串复用),是实现优化非语义;is 比身份(id)、==比值(eq);比值永远用==,is 只给 None/单例/哨兵,绝不依赖缓存行为」。