我在 Python 里用 is 判断两个数字相不相等,小数字时一直好好的,换成大数字后判断突然全错了,我对着这个时对时错的比较排查了大半天的复盘
这是一个让我对 Python 的 is 和 == 彻底分清的故事。我有段代码,要判断两个值是不是相等,我随手用了 is:if a is b:。我测试的时候,用的都是小数字(比如 a = 100, b = 100),a is b 返回 True,一切正常,我就放心地上线了。可线上跑起来,诡异的事发生了:当实际数据是大一点的数字(比如 a = 1000, b = 1000)时,a is b 居然返回 False!明明两个值一模一样,判断却失败了;同样地,用 is 比较两个内容相同的字符串,有时返回 True、有时返回 False,飘忽不定,导致我的逻辑时对时错。
我当时百思不得其解:同样是判断相等,为什么小数字行、大数字就不行?is 到底在比什么?我顺着这个现象深挖,才终于揭开真相,补上了我对 Python 一个最基础、却极易踩的认知漏洞:问题的核心,是我混淆了 is 和 == 这两个本质完全不同的运算符。我一直想当然地以为,"is 和 == 都是判断相等,差不多";可真相是:== 比较的是"值是否相等"(调用 __eq__),而 is 比较的是"是不是同一个对象"(即两者的内存地址(id())是否相同);它们是两码事!那为什么小数字用 is 又"碰巧"对了呢?这才是最坑的地方:Python 为了优化性能,缓存(复用)了一批"常用的小对象"——具体说,是把 -5 到 256 之间的小整数,做成了"单例",全程序共用同一个对象(这叫"小整数池");此外,一些短字符串也会被"驻留(intern)"、复用同一个对象。所以,当 a = 100, b = 100 时,它俩恰好指向了缓存里的同一个对象,is 自然返回 True——但这纯属"缓存的巧合",并不是因为 is 能比较值!而一旦数字超出了 256(如 1000),Python 不再缓存,a = 1000 和 b = 1000 就是两个独立创建的、不同的对象,内存地址不同,is 就如实地返回了 False。我这才痛彻地明白:我用 is 比较值,从一开始就是错的;只是 Python 的小对象缓存,"好心"地用一个巧合,替我掩盖了这个错误,让它在小数据下"碰巧能跑",从而给了我一种虚假的正确感,直到数据变大,真相才暴露。is 和 == 的区别,是 Python 的基础常识,但"小对象缓存"这个性能优化,却让这个区别,变成了一个极其隐蔽、极易让人在小数据下被蒙混过关的陷阱;比较值,永远、永远要用 ==;is 只用来判断"是不是同一个对象",最典型的场景,是判断 x is None。
故障现场:用 is 比较值,被小对象缓存"蒙混过关"
我把这个"时对时错"的现场,摊开给你看:
# ✗ 灾难: 用 is 比较"值是否相等", 被小对象缓存蒙混过关
# 小整数(-5~256): 被缓存复用 → is 碰巧为 True(假象!)
a = 100
b = 100
print(a is b) # ✗ True —— 但这是缓存的巧合, 不是因为 is 能比值!
print(a == b) # ✓ True —— 这才是真正在比值
# 大整数(>256): 不缓存, 是两个独立对象 → is 为 False(真相暴露!)
x = 1000
y = 1000
print(x is y) # ✗ False! 明明值相等, is 却 False
print(x == y) # ✓ True
# 字符串: 短的/字面量常被"驻留"复用, 动态生成的则不一定
s1 = "hello"
s2 = "hello"
print(s1 is s2) # 可能 True(驻留)
s3 = "".join(["h","e","l","l","o"]) # 动态生成
print(s1 is s3) # ✗ 可能 False! 值相同但不是同一对象
print(s1 == s3) # ✓ True
# is 和 == 到底比什么?
# == : 比较"值是否相等"(调用 __eq__)。 ← 比较值就用它!
# is : 比较"是不是同一个对象"(id() 是否相同, 即内存地址)。
# 为什么小数字 is 也对?(最坑的"巧合")
# - Python 缓存了 -5~256 的小整数(小整数池), 全程序共用同一对象。
# - 短字符串也常被驻留(intern)复用。
# - 所以 a=100,b=100 恰好指向同一缓存对象 → is 为 True(纯属巧合)。
# - 超出范围 / 动态生成 → 独立对象 → is 为 False。
# 根因: 用 is 比较值(本应用 ==), 小对象缓存让 is 在小数据下"碰巧对",
# 掩盖了错误; 数据变大/动态生成时, is 如实返回 False, 逻辑就崩。
看着这段"小数字对、大数字错"的代码,我才算彻底想明白了根源。问题的核心,是我用 is 去比较"值是否相等",而这本该用 ==。is 和 == 到底比什么?== 比较"值是否相等"(调用 __eq__)——比较值就用它;is 比较"是不是同一个对象"(id()/内存地址是否相同)。那为什么小数字用 is 也对(最坑的巧合)?因为 Python 缓存了 -5~256 的小整数(小整数池)、全程序共用同一对象,短字符串也常被驻留(intern)复用;所以 a=100, b=100 恰好指向同一缓存对象、is 为 True(纯属巧合);而超出范围(如 1000)或动态生成的字符串,是独立对象,is 就返回 False。归根结底:用 is 比较值(本应用 ==),小对象缓存让 is 在小数据下"碰巧对"、掩盖了错误;数据变大或动态生成时,is 如实返回 False,逻辑就崩——这,就是根源。
第一件事:搞懂 is 与 == 的本质区别
定位到根源,我必须把 is 和 == 的区别,从根上彻底搞清楚:
is 比"对象身份"(内存地址), == 比"值"; 比较值永远用 ==
# 两者的本质:
# == : 调用 __eq__, 比较"值/内容是否相等"。两个不同对象, 值相同 → ==为True。
# is : 比较 id() 是否相同, 即"是不是内存里同一个对象"。
# 类比:
# == : "这两本书内容一样吗?"(可以是两本不同的书, 内容相同)
# is : "这是同一本书吗?"(同一个物理实体)
# 为什么会混: 小对象缓存让 is "碰巧"等于 ==
# - 小整数池: -5 ~ 256 的整数被缓存成单例 → is 这些数碰巧为 True。
# - 字符串驻留: 字面量/标识符样的短字符串被复用 → is 碰巧为 True。
# - 这是"实现细节/性能优化", 不是语言保证! 不能依赖!
# → 同样的代码, 数字大小/字符串来源一变, is 的结果就变。
# 什么时候该用 is?(只有这几种)
# - 判断单例: x is None / x is True / x is False。
# - 判断"是不是同一个对象"(故意要比身份, 如缓存命中、循环引用检测)。
# - 枚举/哨兵对象的比较。
# 什么时候该用 ==?
# - 比较值! 数字、字符串、列表内容、任何"内容是否相等"的判断。
# - 99% 的"相等判断", 都该用 ==。
# 特例: None / True / False 是单例, 用 is 判断最规范、最快。
# if x is None: ✓ 推荐(PEP8 也这么建议)
# if x == None: △ 能用但不规范(且自定义 __eq__ 可能出意外)
# 关键认知: 比较值用 ==, 判断"同一对象/单例"用 is; 别被小对象缓存骗了。
# 核心: is 比对象身份(地址)、== 比值; 比较值永远用 ==, is 只用于 None/单例;
# 小整数池和字符串驻留会让 is 在小数据下"碰巧对", 是陷阱别依赖。
原理终于清晰了。两者的本质:== 调用 __eq__、比较"值/内容是否相等"(两个不同对象、值相同,== 为 True);is 比较 id() 是否相同、即"是不是内存里同一个对象"。打个比方:== 是问"这两本书内容一样吗"(可以是两本不同的书),is 是问"这是同一本书吗"(同一个物理实体)。为什么容易混?因为小对象缓存让 is "碰巧"等于 ==:小整数池(-5~256 被缓存成单例)、字符串驻留(短字符串被复用),让 is 这些值碰巧为 True;但这是"实现细节/性能优化",不是语言保证,绝不能依赖——数字大小/字符串来源一变,is 的结果就变。那什么时候该用 is?只有几种:判断单例(x is None/x is True/x is False)、故意要比"是不是同一个对象"、枚举/哨兵对象比较。什么时候该用 ==?比较值!数字、字符串、列表内容……99% 的"相等判断"都该用 ==。特别地:None/True/False 是单例,用 is 判断最规范、最快(if x is None 是 PEP8 推荐写法)。由此,我刻下一个关键认知:比较值用 ==,判断"同一对象/单例"用 is;别被小对象缓存骗了。归根结底:is 比对象身份(地址)、== 比值;比较值永远用 ==,is 只用于 None/单例;小整数池和字符串驻留会让 is 在小数据下"碰巧对",是陷阱别依赖。
第二件事:正解——比较值用 ==,is 只留给 None/单例
搞懂了原理,正解就极其简单了:比较值,一律用 ==;is 只用来判断 None 等单例。
# ✓ 正解一: 比较值, 一律用 ==
a = 1000
b = 1000
if a == b: # ✓ 比值, 永远正确, 不受缓存影响
print("相等")
s1 = "hello"
s3 = "".join(["h","e","l","l","o"])
if s1 == s3: # ✓ 比值, 动态生成的字符串也正确
print("字符串相等")
# ✓ 正解二: is 只用于判断 None / True / False 单例
def f(x=None):
if x is None: # ✓ 推荐: 判 None 用 is(规范、快、不被 __eq__ 干扰)
x = []
# ✗ 别写 if x == None: 不规范, 且若 x 自定义了 __eq__ 可能出意外
return x
# ✓ 正解三: 判断布尔单例(谨慎, 通常直接用真值判断更好)
if flag is True: # 极少这么写, 一般直接 if flag:
...
# ✓ 正解四: 故意要比"是不是同一个对象"时, 才用 is
cache = {}
obj = cache.get(key)
if obj is sentinel: # ✓ 用哨兵对象判断"是否命中", 比身份是合理的
...
# 一句话决策:
# - 问"值一样吗?" → ==
# - 问"是同一个东西吗?" / "是不是 None?" → is
# ⚠ 自定义类: 默认 == 等同于 is(比地址), 想按值比较要重写 __eq__(见 Java equals 篇同理)。
# 核心: 比较值一律用 ==(不受小对象缓存影响, 永远正确);
# is 只留给 None/单例判断, 或故意比较对象身份的场景。
修复其实一字之差,却根除了问题。正解一,比较值一律用 ==:无论是大整数 1000 还是动态生成的字符串,== 比的是值,永远正确、完全不受缓存影响。正解二,is 只用于判断 None 等单例:if x is None: 是推荐写法(规范、快、且不会被自定义的 __eq__ 干扰)——别写 if x == None(不规范,且若对象自定义了 __eq__ 可能出意外)。正解三/四:判断布尔单例(虽然一般直接 if flag: 更好)、或故意要比"是不是同一个对象"(如用哨兵对象判断缓存是否命中)时,才用 is。一句话决策:问"值一样吗?"→ ==;问"是同一个东西吗?/是不是 None?"→ is。还有个关联点:自定义类的 默认 == 其实等同于 is(比地址),想按值比较要重写 __eq__(和 Java 重写 equals 同理)。归根结底:比较值一律用 ==(不受小对象缓存影响、永远正确);is 只留给 None/单例判断,或故意比较对象身份的场景。
第三件事:小对象缓存背后,Python 的几个"性能优化巧合"
这次踩坑让我意识到,Python 里还有不少"性能优化带来的、看似神奇却不能依赖"的行为。我把它们梳理了一遍:
# Python 里几个"碰巧成立、却不能依赖"的实现细节:
# 1. 小整数池: -5 ~ 256 被缓存
print(256 is 256) # True (缓存) ← 别依赖
print(257 is 257) # 可能 False (看上下文/解释器)
# → 这是 CPython 的优化, 不是语言规范; 比较数值永远用 ==。
# 2. 字符串驻留(interning): 标识符样的短字符串被复用
a = "hello"; b = "hello"
print(a is b) # 常 True (驻留) ← 别依赖
c = "hello world!" # 含空格/特殊字符的, 不一定驻留
print(c is "hello world!") # 可能 False
# → 比较字符串永远用 ==。
# 3. 不可变对象可能被复用(元组等), 但同样不保证。
# 这些"缓存/复用"的目的:
# - 节省内存、加速创建(小整数、常用字符串用得极频繁)。
# - 是 CPython 的实现优化, 其他解释器(PyPy等)行为可能不同。
# 你能依赖的 vs 不能依赖的:
# ✓ 能依赖: == 比值的结果、is None 判断 None。
# ✗ 不能依赖: is 比较两个"值相等的非单例对象"的结果(看缓存, 不确定)。
# 一个验证小工具: 用 id() 看是不是同一对象
print(id(a), id(b)) # 相同 = 同一对象(is 为 True); 不同 = is 为 False
# 核心: 小整数池/字符串驻留是 CPython 的性能优化(碰巧让 is 成立), 不是语言保证;
# 绝不能依赖它们; 比较值永远 ==, 判 None 用 is。
原来 Python 里这类"碰巧成立的实现细节"还不少。小整数池:-5~256 被缓存,256 is 256 是 True、但 257 is 257 可能 False;字符串驻留:标识符样的短字符串被复用、is 常为 True,但含空格/特殊字符的不一定驻留;不可变对象(元组等)也可能被复用,但同样不保证。这些"缓存/复用"的目的,是节省内存、加速创建(小整数、常用字符串用得极频繁),但这是 CPython 的实现优化,其他解释器(PyPy 等)行为可能不同。所以要分清能依赖的 vs 不能依赖的:✓ 能依赖:== 比值的结果、is None 判断 None;✗ 不能依赖:is 比较两个"值相等的非单例对象"的结果(看缓存、不确定)。(想验证两个变量是不是同一对象,可以用 id() 看地址。)它给我的启发是:编程语言里,有"语言规范保证的行为"和"某个实现碰巧表现出的行为"——前者你可以放心依赖,后者绝不能;而区分这两者,正是写出"可移植、可靠"代码的关键。is 在小整数上"碰巧对",就是典型的"实现细节",依赖它,就是在把代码的正确性,建立在一个随时可能变化的沙地上。
下面这张图,是这次"is 比较值"事故的成因与解法:
第四件事:is 与 == 的使用场景速查
这次踩坑后,我把 is 和 == 的使用场景,整理成一张速查表,以后一看就知道该用哪个。
| 场景 | 该用 | 说明 |
|---|---|---|
| 判断两个数字/字符串值相等 | == | 比值, 不受缓存影响 |
| 判断列表/字典内容相等 | == | 逐元素比值 |
| 判断 x 是不是 None | is None | 规范、快、不受 __eq__ 干扰 |
| 判断 x 是不是 True/False | 一般直接 if x / 必要时 is | 少用 == True |
| 判断是不是同一个对象 | is | 故意比身份(缓存命中/哨兵) |
| 自定义对象按值比较 | == (需重写 __eq__) | 默认 == 等同 is, 要重写才比值 |
这张表,把"该用 is 还是 =="彻底说清了。规律极其简单:绝大多数"相等判断"(数字、字符串、列表、字典的值相等)都用 ==;is 几乎只在一个高频场景出现——判断 x is None(这是规范、最快、且不受自定义 __eq__ 干扰的写法),其余就是故意要比"对象身份"的少数场景(缓存命中、哨兵对象)。有两个小提醒:判断布尔,一般直接 if x: 就好,少用 == True(更别用 is True 除非真要严格区分);自定义对象想按值比较,记得重写 __eq__(因为默认的 == 等同于 is、比的是地址)。它给我的启发是:很多语言的基础运算符,看起来简单,却各有精确的语义边界;is 和 == 就是一对最典型的例子——记住"值用 ==、身份/None 用 is"这条简单的规则,就能避开绝大多数相关的坑。基础不牢,地动山摇;而把这些"看似简单"的基础,真正搞精确,正是写出可靠代码的地基。
第五件事:Python 里其他"小数据下碰巧能跑"的隐蔽坑
这次"小数字碰巧对"的经历,让我警觉:Python 里还有不少"在小数据/特定条件下碰巧能跑、换个场景就崩"的坑。我把它们一并梳理了。
| 坑 | 碰巧能跑的场景 | 翻车的场景 |
|---|---|---|
| 用 is 比较值 | 小整数/短字符串(缓存) | 大整数/动态字符串(本文) |
| 可变默认参数 | 只调一次时 | 多次调用共享同一默认对象 |
| 依赖 dict 有序 | Py3.7+ 碰巧有序 | 老版本/依赖排序逻辑时 |
| 浮点 == 比较 | 简单整数值的浮点 | 0.1+0.2 != 0.3 等精度问题 |
| 遍历时修改集合 | 元素少/没触发扩容 | 元素多时 RuntimeError |
| 捕获循环变量的闭包 | 循环内立即用 | 留到循环后用(全取末值) |
这张表,让我看清了这类坑共同的、最阴险的特征:它们不是"一上来就报错",而是"在某些条件下碰巧能正常工作"——而这份"碰巧能跑",恰恰是最危险的:它给了你虚假的信心、骗过了你的测试(如果测试数据恰好落在"碰巧对"的范围内),然后埋伏在代码里,等数据或条件一变,才在线上、在你最意想不到的时候,暴露出来。无论是is 比较值(小整数碰巧对)、可变默认参数(只调一次碰巧对)、依赖 dict 有序(新版本碰巧对)、浮点 ==(简单值碰巧对)、遍历时改集合(元素少碰巧对)、还是闭包捕获循环变量(立即用碰巧对)——它们都在用"局部的、偶然的正确",掩盖着"全局的、本质的错误"。它给我的最大启发是:测试通过、demo 能跑,绝不等于代码是对的;尤其要警惕那些"在我测试的这组数据下能跑"的代码——它可能只是碰巧对了。真正可靠的代码,要建立在"理解其行为在所有情况下都正确的原理"之上,而不是"它在我试的几个例子上没出错"这种脆弱的经验之上。知其然,更要知其所以然——这,是避开这一整类"碰巧能跑"陷阱的唯一办法。
第六件事:写一个相等判断时,我现在会怎么决策
现在,每当我要写一个"相等判断",脑子里都会过一遍这张(其实很简单的)决策图——核心就一问:我是在问"值一样吗"还是"是不是同一个东西"?
这张图虽小,却能挡住一整类坑。核心就一个判断:我想判断什么?——值/内容是否相等 → ==;是不是 None → is None;是不是同一个对象/单例 → is。对 ==,还要多看一眼:是自定义对象吗?是的话,确保重写了 __eq__(否则 == 会退化成比地址);内置类型直接 == 即可。对 None:用规范的 if x is None,别写 == None。这套判断,简单到几乎是条件反射,但它能让我永远不再被"小对象缓存"的巧合骗到——核心始终是那句:分清"值相等"和"同一个对象",绝大多数时候,你要的都是前者(==)。
我立下的几条规矩
这场"is 比较值时对时错"的事故,换来了我写 Python 时,刻进骨子里的几条铁律:
- 比较值,永远用 ==。数字、字符串、列表、字典的内容相等判断,一律 ==,不受小对象缓存影响。
- is 只用于 None/单例/比对象身份。最高频的是 if x is None;其余就是故意比"是不是同一个对象"。
- 判 None 用 is None,别用 == None。规范、快、且不会被自定义 __eq__ 干扰。
- 别依赖小整数池/字符串驻留。is 在小整数/短字符串上"碰巧对"是 CPython 实现细节,不是语言保证,绝不能依赖。
- 自定义对象要按值比较就重写 __eq__。默认 == 等同 is、比地址(配合 hashCode 篇的 Java 同理)。
- 警惕"碰巧能跑"的代码。测试数据恰好落在"碰巧对"的范围,会给你虚假信心;要理解原理,别靠几个例子没出错。
- 知其然更知其所以然。可靠不是"它在我试的例子上对了",而是"我理解它在所有情况下都对"。
附:几行代码亲眼看清 is 的"碰巧"
口说无凭。下面这几行,用 id() 亲眼看清 is 为什么"小数字对、大数字错",跑一遍胜过千言:
# 实验1: 小整数被缓存 → is 碰巧 True
a = 100
b = 100
print(a is b, id(a) == id(b)) # True True —— 同一个缓存对象
# 实验2: 大整数不缓存 → is False(值相等但不同对象)
x = 1000
y = 1000
print(x is y, id(x) == id(y)) # False False —— 两个独立对象!
print(x == y) # True —— 值是相等的
# 实验3: 找出小整数池的边界(-5 ~ 256)
print(256 is 256) # True (在池内)
n1 = 257; n2 = 257
print(n1 is n2) # 常 False (出池了, 取决于上下文)
# 实验4: 字符串驻留 vs 动态生成
s1 = "hello"
s2 = "hello"
print(s1 is s2) # 常 True (驻留)
s3 = "hel" + "lo" # 编译期可优化, 可能 True
s4 = "".join(["h","e","l","l","o"]) # 运行期动态生成
print(s1 is s4, s1 == s4) # False True —— is 错, == 对!
# 实验5: None 是单例, is 永远可靠
print(None is None) # True (永远)
x = None
print(x is None) # True ✓ 这才是 is 的正确用法
# 核心: id() 揭示真相 —— is 比的是 id(地址), 小整数/短字符串碰巧同 id 才 is True;
# 大整数/动态字符串 id 不同 is 就 False; 比较值永远 ==, 判 None 才用 is。
这几行代码,把 is 的"碰巧",用 id() 照得清清楚楚。实验 1 和 2 是最直接的对比:a=100, b=100 时 id(a) == id(b)(同一个缓存对象,所以 is True);而 x=1000, y=1000 时 id(x) != id(y)(两个独立对象,所以 is False),但 x == y 是 True(值确实相等)——id() 一打印,真相大白:is 比的根本是地址,跟值没关系。实验 3 还能亲手探出小整数池的边界(256 在池内、257 出池);实验 4 展示了字符串驻留 vs 动态生成的差别(s1 is s4 是 False、但 s1 == s4 是 True);实验 5 则印证了 None is None 永远可靠——这才是 is 的正确用武之地。这,正是我想用这几行代码,留给每一个学 Python 的人的最后一课:当你对一个行为感到困惑时(尤其是 is 这种"有时对有时错"的),别去猜、别去背结论,而是用 id()、type() 这些"探针",把对象的真实身份打印出来,让 Python 亲口告诉你它内部到底是怎么回事。一次"用 id() 看个明白"的实验,胜过十遍"记住 is 和 == 的区别"的死记硬背;而那些通过亲手探查得来的理解,会真正变成你的、再也忘不掉的认知。
写在最后
回头看,这场由 is 和 == 混用引发的事故,真正教给我的,是一个比"分清这俩运算符"本身更深的道理:编程中最危险的错误,往往不是那些"一上来就崩"的错误(它们反而容易被发现和修复),而是那些"在大多数情况下碰巧表现正确、只在特定边界条件下才暴露"的错误——因为它们能骗过你的测试、骗过你的直觉,潜伏到生产环境,在最坏的时机给你致命一击。我的 is 错误,正是被 Python 那份"好心"的小对象缓存,用一个美丽的巧合,完美地伪装了起来;它在我所有的小数据测试里,都"表现得无可挑剔",让我对一个根本性的错误,毫无察觉。这让我深刻地意识到:"它能跑"和"它是对的",是两件截然不同的事;而一个工程师的成熟度,很大程度上体现在他是否能看穿"碰巧能跑"的假象,去追问那个"它为什么对、又会在什么条件下不对"的本质原理。所以,写代码时,要时刻对那份"它跑通了"的轻松保持一丝警惕,多问自己一句:"它是真的对,还是碰巧对?它的正确性,建立在我理解的原理上,还是建立在我试过的几个例子上?"。追求"理解原理上的正确",而非"碰巧能跑的正确"——这,是我用一次"is 比较值"的事故,换来的、关于 Python、也关于编程严谨性的、最朴素也最深刻的领悟。如果这篇复盘,能让你以后写相等判断时,条件反射地用对 == 和 is,并对"碰巧能跑"多一分警惕,那我对着那个时对时错的判断熬的这大半天,就值了。
—— 别看了 · 2026