我在 Python 里用 is 来判断两个数是否相等,小数值时一直好好的,某次数值大了一点 is 突然返回 False,因为 is 比的根本不是值相等而是"是不是同一个对象",只是小整数被缓存了才碰巧一致:一次混淆"身份"与"相等"、被假性等价骗了很久的深度复盘
那个"明明两个数一样,判断却说不等"的诡异 bug,让我重新认识了 Python 的 is。我有段代码判断一个数值是否等于某个特定值,写成了 if count is 1000:——我以为 is 就是"判断相等"。在开发和小数据测试时,它一直好好的。可上了线、数据一变,就出诡异问题:有时 count 明明就是 1000,count is 1000 却返回 False,判断逻辑整个错乱。我盯着 count == 1000 是 True、count is 1000 却是 False 的现场,百思不得其解。复盘 is 的本质,我才彻底搞懂,后背发凉:问题出在我把 is 当成了"判断值是否相等",但 is 比较的根本不是"值",而是"是不是同一个对象(身份)"。Python 里,== 比较的是"两个对象的值是否相等"(调 __eq__);而 is 比较的是"两个变量是不是指向内存里同一个对象(身份,比的是 id())"——这是两件完全不同的事;那为什么我用 is 比小数值时"一直没事"?因为 Python 出于性能,缓存(intern)了小整数(-5 到 256):这个范围内的整数,全局共用同一个对象,所以 a = 100; b = 100 时 a is b 碰巧为 True(它俩真是同一个对象);可一旦超出这个缓存范围(比如 1000),每次出现的 1000 可能是不同的对象,count is 1000 就返回 False 了——值相等,但不是同一个对象;也就是说:在小整数范围内,is 碰巧和 == 表现一致,这个"假性等价"骗了我很久,让我误以为 is 就是判断相等,直到数值超出缓存范围才暴露。根本原因是:is 比较的是对象身份(是否同一对象、看 id),== 比较的是值;我误用 is 判断值相等,因小整数(-5~256)被缓存共用同一对象、is 碰巧与 == 一致而长期没暴露,值超出缓存范围后 is 就返回 False。问题的根,是混淆了 is(身份/同一对象)和 ==(值相等)——用 is 判断值相等,被小整数缓存造成的"碰巧一致"假象掩盖,直到值超出缓存范围才暴露。这篇就把这次"is vs =="的坑,从头到尾复盘一遍。
故障现场:== 是 True,is 却是 False
问题在于 is 比的是身份不是值,小整数缓存造成了假象:
# 我的写法: 用 is 判断值相等(以为 is 就是"等于")
count = compute()
if count is 1000: # ✗ is 比的是"是否同一对象", 不是"值是否相等"!
do_something()
# 诡异现象:
a = 100
print(a == 100) # True
print(a is 100) # True ← 小整数(-5~256)被缓存, 碰巧是同一对象, is也True(假象!)
b = 1000
print(b == 1000) # True
print(b is 1000) # False ← 1000超出小整数缓存, 不是同一对象, is是False!
# (注: 具体结果还和Python实现、是否在同一编译单元有关, 更不可靠)
s1 = "hello"
print(s1 is "hello") # 可能True(短字符串被intern缓存)
s2 = "hello world!" * 1
print(s2 is "hello world!") # 可能False(长/动态字符串不一定intern)
"""
为什么会这样:
- == 比较【值】: 调 __eq__, 判断"两个对象的值是否相等"(1000 == 1000 → True);
- is 比较【身份】: 判断"两个变量是否指向内存里【同一个对象】"(比 id(a) == id(b));
- 它俩是两件不同的事: 值相等 不一定 是同一对象; 同一对象 必然 值相等。
为什么用 is 比小数值"碰巧"对:
- Python 缓存(intern)了小整数 -5 到 256: 这范围内的整数全局共用同一对象;
所以 100 is 100 → True(它俩真是同一个缓存对象);
- 也intern了一些短字符串/标识符样式的字符串;
- 于是在这些"被缓存的值"上, is 碰巧和 == 表现一致 → 误导你以为 is 就是判断相等;
- 一旦超出缓存范围(1000)、或动态生成的对象, is 就和 == 分道扬镳 → 暴露bug。
★ 核心: == 比值, is 比身份(是否同一对象, 看id);
比较"值是否相等"必须用 ==; is 只用于判断"是不是同一个对象"——主要是和单例比: is None / is True / is False;
小整数和短字符串的缓存, 让 is 在它们上"碰巧"等价于 ==, 这是个会骗人很久的假象。
"""
看着 count == 1000 为 True、count is 1000 却为 False 的现场,我又错愕又恍然:"我一直用 is 当'等于'用,测试时数值都小、一直对……谁知道 is 比的是'是不是同一个对象',只是小整数被缓存了才碰巧和 == 一样!这个'碰巧一样'把我骗得死死的。"这个坑最隐蔽的地方在于:它在小整数(-5~256)、短字符串上一直表现正确,给你"is 就是判断相等"的强烈错觉(还被无数次"碰巧对"的经验强化);它只在值超出缓存范围、或对象动态生成时才暴露,而这往往是上线后、数据变大时——开发期极难发现。下面就来拆解,is 和 == 到底该怎么用。
第一件事:搞懂身份(is)与相等(==)
我顺着这次事故,把 is 和 == 的区别彻底理清了。
is 和 == 到底差在哪? 各自该用在哪?
【核心: ==比"值是否相等"(调__eq__), is比"是否同一对象"(比id); 比较值用==, is只用于和单例比
(is None/is True/is False); 小整数(-5~256)和短字符串被缓存, 让is在它们上碰巧等价于==, 是假象】
1. == 比较"值":
- 调对象的 __eq__ 方法, 判断"两个对象的值在语义上是否相等";
- 1000 == 1000 → True; [1,2] == [1,2] → True; "a" == "a" → True;
- 这是"判断相等"该用的。
2. is 比较"身份(identity)":
- 判断"两个变量是否指向内存里【同一个对象】", 等价于 id(a) == id(b);
- 同一对象 → 必然值相等; 但值相等 → 不一定是同一对象;
- 这是"判断是不是同一个东西"用的。
3. 为什么用 is 比值会"时灵时不灵":
- Python 缓存(intern)了小整数 -5~256 和部分短字符串 → 这些值全局共用同一对象;
- 所以在这些值上, is 碰巧和 == 一致(因为值相等的它们恰好就是同一对象);
- 超出缓存(大整数)、动态生成的对象、可变对象 → 值相等但不是同一对象 → is 为 False;
- 这种"小范围碰巧一致、大范围分道扬镳"的行为, 最具迷惑性。
4. is 该用在哪(只用于和"单例"比身份):
- is None / is not None (None是单例, 全局唯一对象, 判None必用is, 不用==);
- is True / is False (判断是否就是布尔单例本身, 而非真假性);
- 判断"两个变量是否指向同一对象"(确实需要身份比较时)。
- 除此之外, 比较值一律用 ==。
5. 为什么 None 用 is 不用 ==:
- None 是单例(全局唯一), is None 判身份, 又快又准、不受 __eq__ 重写影响;
- 用 == None 可能被对象重写的 __eq__ 干扰(虽少见), 且不符合惯例; PEP8 也规定用 is None。
6. 本质: "是不是同一个东西(身份)" 和 "值相不相等" 是两个不同的概念
- 别因为它们在某些情况下(小整数缓存)碰巧一致, 就把它们当成一回事;
- 用错(拿 is 当 ==)平时可能没事, 但本质是错的, 边界情况必暴露。
一句话: ==比值(调__eq__)、is比身份(同一对象/id); 比较值用==, is只用于和单例比(is None/True/False);
小整数(-5~256)和短字符串被缓存让is在它们上碰巧等价于==、是会骗人很久的假象, 别拿is当==。
这套认知,是整个坑的根。== 比值:调 __eq__ 判断值是否语义相等——判断相等该用它。is 比身份:判断是否指向同一对象(等价 id 相等)——同一对象必值相等,值相等不一定同一对象。为什么时灵时不灵:Python 缓存小整数(-5~256)和短字符串,这些值上 is 碰巧和 == 一致,超出缓存或动态对象就分道扬镳。is 用在哪:只用于和单例比身份——is None / is True / is False、判断是否同一对象;比较值一律用 ==。本质:"是不是同一个东西(身份)"和"值相不相等"是两个不同概念,别因小整数缓存碰巧一致就当一回事。一句话:== 比值(调 __eq__)、is 比身份(同一对象/id);比较值用 ==,is 只用于和单例比(is None/True/False);小整数(-5~256)和短字符串被缓存让 is 在它们上碰巧等价于 ==、是会骗人很久的假象,别拿 is 当 ==。
第二件事:正解——比值用 ==,is 只和单例比
知道了 is 比身份,正解就清楚了:比较值一律用 ==,is 只留给 None/单例。
# 正解1: 比较值用 ==(本次该做的)
count = compute()
if count == 1000: # ✓ 比值用 ==, 无论数值大小都正确
do_something()
if name == "abc": # ✓ 比字符串值用 ==
...
if items == [1, 2, 3]: # ✓ 比列表内容用 ==(调__eq__逐元素比)
...
# 正解2: 判断 None 用 is None(None是单例, 这是is的正确用法)
if result is None: # ✓ 判None必用is(快、准、符合PEP8)
...
if result is not None: # ✓
...
# 正解3: 判断布尔单例本身用 is(少见, 通常直接用真假性即可)
if flag is True: # 仅当你要判"它就是True这个对象"时; 通常 if flag: 就够
...
# 注意: 一般判真假别写 == True / is True, 直接 if flag: / if not flag:
# 正解4: 判断"是不是同一个对象"才用 is(确实需要身份比较时)
if a is b: # ✓ 想知道a和b是否指向同一对象(如缓存/单例/去重)
...
# 反例(别这样):
# if count is 1000: # ✗ 拿is当==, 小整数碰巧对、大整数就错
# if name is "abc": # ✗ 拿is比字符串, intern碰巧对、动态字符串就错
# if x == None: # △ 能跑但不规范, 判None用 is None
# 提示: 现代静态检查(flake8/pylint)会警告 "is" 用于字面量比较(F632), 善用它揪出这类错。
# 核心: 比较"值是否相等"一律用 ==; is 只用于和单例比身份(is None / is True/False)、
# 或确实要判"是不是同一对象"; 别拿 is 当 ==, 别被小整数缓存的"碰巧一致"骗了。
这套正解的关键,是把"比值"和"比身份"两件事用对的运算符分开。比较值用 ==:无论数值大小、是字符串还是列表,比值一律用 ==——这正是本次该做的。判 None 用 is None:None 是单例,这是 is 的正确用法,快、准、符合 PEP8。判真假直接用真假性:if flag: 就够,别写 == True / is True。判同一对象才用 is:确实需要身份比较(缓存/单例/去重)时。善用静态检查:flake8/pylint 的 F632 会警告"is 用于字面量比较",能在写代码时就揪出这类错。
第三件事:其他几个"被假性等价骗了"的坑
顺着这次 is/==,我把"两件本质不同的事,因常见情况碰巧一致而被混淆"的几类坑也一并理了:
几类"被假性等价骗了"的坑(核心都是"碰巧一致掩盖了本质区别"):
坑1: == 和 equals/相等的多种含义(同581 BigDecimal)——浮点 0.1+0.2 == 0.3 为False(精度);
"看起来相等"和"真的相等"在浮点上不一致, 别用==比浮点, 用误差范围。
坑2: 浅拷贝当深拷贝——copy() 浅拷贝, 嵌套对象还是共享; 简单数据时"碰巧"像深拷贝, 嵌套就露馅。
坑3: 可变默认参数(同350)——def f(x=[]) 的 [] 只创建一次; 单次调用"碰巧"对, 多次调用共享就出bug。
坑4: 整数除法 vs 真除——Python2里 / 对整数是地板除; 能整除时"碰巧"对, 除不尽就错(Py3已修正)。
坑5: 把"引用相等"当"内容相等"(其他语言: Java的 == vs equals, JS的 === 对对象)——
两个内容相同的对象, 引用比较为False; 小心别拿引用比较当内容比较。
坑6: 时区一致时把naive时间当对的(同572)——本地和服务器同时区时"碰巧"对, 跨时区就错。
共同的根: 两件【本质不同】的事(身份vs相等、浅vs深、引用vs内容), 在【常见情况下表现相同】,
会让人误以为它们是【同一回事】; 而这种"假性等价"极其隐蔽——因为它被大量"碰巧正确"的经验
反复强化, 让你越用越确信; 直到某个边界情况(大整数、嵌套、跨时区)让它们分道扬镳, 才暴露。
这些坑看似不同,根却是同一个:两件本质不同的事(身份 vs 相等、浅 vs 深、引用 vs 内容),在常见情况下表现相同,会让人误以为它们是同一回事;而这种"假性等价"极其隐蔽——它被大量"碰巧正确"的经验反复强化,直到边界情况才暴露。认清这个根("警惕碰巧一致掩盖的本质区别"),才不会被"一直没出错"的假象养出错误的认知。
第四件事:is vs == 行为 / 该用哪个——两张对照表
我把 is 和 == 在各种值上的行为、以及该用哪个,整理成对照表,贴在了团队的 Python 规范里:
| 表达式 | == 结果 | is 结果 | 原因 |
|---|---|---|---|
| 100 与 100 | True | True | 小整数缓存,同一对象 |
| 1000 与 1000 | True | 常 False | 超出缓存,不同对象 |
| "abc" 与 "abc" | True | 常 True | 短字符串 intern |
| 动态生成的字符串 | True | 常 False | 未 intern,不同对象 |
| [1,2] 与 [1,2] | True | False | 两个不同的列表对象 |
| x 与 None | (可用) | True/False | None 单例,该用 is |
| 需求 | 用 | 说明 |
|---|---|---|
| 判断值是否相等 | == | 调 __eq__,比值 |
| 判断是否为 None | is None | 单例身份比较 |
| 判断真假 | if flag: | 别写 == True / is True |
| 判断是否同一对象 | is | 缓存/去重/单例场景 |
| 比较浮点 | abs(a-b) < eps | 别用 ==(精度,同 554) |
这两张表的核心,第一张是== 永远按值给出正确结果,而 is 的结果依赖"是否同一对象"这个和缓存实现相关的细节——在小整数/短字符串上碰巧对、别处就错;第二张是比值用 ==、判 None 用 is None、判真假直接 if、比浮点用误差。记住一条:除了 is None(及和单例比身份),其他比较几乎都该用 ==。
第五件事:关于 is 与 == 的几组容易想当然的认知
这次事故也让我厘清了几组关于 is/== 的、容易想当然的概念:
| 直觉以为 | 实际上 |
|---|---|
| is 就是判断相等 | is 判身份(同一对象),== 才判值相等 |
| is 比 == 更快更准 | 比值时 is 是错的,只是小值碰巧对 |
| a is b 为 True 就是值相等 | 反过来成立,但值相等不一定 is |
| 用 is 比数一直没出错 | 是小整数缓存的假象,大数就错 |
| 字符串可以用 is 比 | intern 碰巧对,动态字符串就错 |
| 判 None 用 == None 也行 | 该用 is None(快、准、规范) |
| == 和 is 差不多 | 本质不同,只是小范围碰巧一致 |
这张表里,我栽的是第一行和第四行:把 is 当成了"判断相等",又因为"用 is 比小数一直没出错"而强化了这个错误认知,直到数值变大才暴露。厘清这些,核心是一个意识:"是不是同一个对象(身份)"和"值相不相等"是两个不同的概念;is 和 == 分别对应它们,不能混用;小整数和短字符串的缓存制造了"碰巧一致"的假象——别让这个假象,把你对两个本质不同概念的认知给搅混了。
第六件事:写比较时,我现在的自检习惯
现在每当我要写一个比较,我都会先按这张图问自己:
这张图的精髓,是"想清楚比值还是比身份:比值用==、判None用is None、比浮点用误差"。先问我要判值相等还是同一对象、比值就==(浮点用误差)、判 None 就is None。这套习惯,让我从"随手用 is 当等于"变成了"想清楚到底在比什么"——核心始终是:== 比值、is 比身份;比较值用 ==,is 只用于和单例比(is None/True/False)或判同一对象;小整数和短字符串的缓存让 is 碰巧等价于 == 是假象,别拿 is 当 ==。
我立下的几条规矩
这场"== 是 True、is 却是 False"的事故,换来了我写 Python 时,刻进骨子里的几条铁律:
- == 比较"值是否相等"(调 __eq__);is 比较"是否同一对象/身份"(等价 id 相等)。
- 比较值一律用 ==,不论数值大小、字符串还是容器。
- is 只用于和单例比身份:is None / is not None(判 None 必用 is)、is True/False、或判同一对象。
- 小整数(-5~256)和短字符串被缓存,让 is 在它们上碰巧等价于 ==——这是会骗人很久的假象。
- 判真假直接 if flag / if not flag,别写 == True / is True。
- 比较浮点别用 ==,用 abs(a-b) < 误差(同 554)。
- 开 flake8/pylint,F632(is 比字面量)等会帮你揪出这类错。
附:几个一跑就明白的小实验
为了让团队直观感受 is 和 == 的区别(以及小整数缓存的"骗局"),我写了几个一跑就明白的小实验,贴在内部 wiki。
# 实验1: 小整数缓存 —— is 在 -5~256 内"碰巧"和 == 一致
a, b = 256, 256
print(a == b, a is b) # True True ← 256在缓存内, 同一对象
a, b = 257, 257
print(a == b, a is b) # True False ← 257超出缓存, 不同对象! is翻脸了
# 实验2: 用 id() 看清"是不是同一个对象"
x = 1000
y = 1000
print(x == y) # True
print(id(x) == id(y)) # 常为False —— 它们是值相等、但id不同的两个对象
print(x is y) # 等价于上面的 id 比较
# 实验3: 字符串 intern 的"碰巧"
s1 = "hello"
s2 = "hello"
print(s1 is s2) # 常True(短字符串被intern)
s3 = "".join(["h", "e", "l", "l", "o"]) # 动态生成
print(s1 == s3, s1 is s3) # True False ← 值相等, 但不是同一对象
# 实验4: None 才是 is 的正确舞台
v = None
print(v is None) # True ✓ (判None的正确姿势)
# 跑完这几个, 那种"咦, 257怎么就is False了"的意外, 比任何文档都让人记住:
# is 比的是身份, 不是值; 小整数缓存制造了"碰巧一致"的假象。
这几个实验的价值,在于把"小整数缓存造成的假性等价"变成了可以亲眼看到的反常输出——256 is 256 为 True 而 257 is 257 为 False。那种"就差 1,结果怎么反了"的意外,会牢牢地把"is 比身份不比值"刻进脑子里。我越来越觉得:对这类"被假象掩盖的本质区别",与其反复讲道理,不如让人亲手跑一个"结果出乎意料"的小实验——意外,是最好的老师。
写在最后
回头看,这场由"拿 is 当 == 用"引发的、值相等却判不等的事故,真正教给我的,远不止"比值用 ==、判 None 用 is"这一个技巧。它让我对"两件本质不同的事, 如果在我们最常遇到的情况下表现得一模一样, 就会悄悄地在我们心里被当成同一回事; 而这种'假性等价'是一种极其顽固的认知错误——因为它不断被'碰巧正确'的经验所证实和强化, 让我们越用越笃定, 直到一个边界情况把它们撕开",有了一次刻骨的体会。我栽跟头,是因为我把"它俩在小整数上碰巧一样"的经验, 当成了"它俩本来就一样"的结论——每一次"用 is 比小数也对", 都在我心里给"is 就是等于"这个错误认知添了一块砖;我没意识到: "在我试过的范围内一直一致" 根本不能推出 "它们本质相同"——它们只是在那个范围内, 因为一个我不知道的巧合(小整数缓存)而恰好重合;是那个一直成立的"巧合", 把我对两个不同概念的混淆, 伪装成了正确, 还喂养得越来越壮。这让我领悟到一个关于"表象一致与本质区别"的深刻认知:"两个东西在某个范围内表现一致" 和 "两个东西本质上是同一个东西", 是必须区分的两件事; 前者可能只是"巧合的重合", 后者才是"本质的相同";而最危险的认知陷阱, 恰恰是那种"在常见情况下一直成立、于是被反复验证、让你深信不疑"的"假性等价"——它比"一上来就出错"的错误隐蔽得多、顽固得多, 因为你有大量"它没错过"的经验为它背书;"归纳出的'一直如此', 不等于'本质必然如此'"——边界之外, 那个被巧合掩盖的本质区别, 终会现形。这给了我一种面对"看起来一样的东西"时的清醒:当两样东西"用起来好像一样"时,不要满足于"反正一直没出过错",而要追问"它们本质上是同一个东西吗?还是只是在我遇到的情况下碰巧一致?它们的定义/原理到底一不一样?"——去理解它们各自的本质(is 比身份、== 比值), 而不是用'表象的一致'代替'本质的理解';"区分'碰巧表现一致'和'本质相同', 不被被经验反复强化的假性等价蒙蔽",是避免在边界处突然翻车的关键。认清表象一致不等于本质相同、假性等价被碰巧正确的经验顽固强化、要理解本质而非满足于表象一致——这,是我用一次 is 当 == 的事故,换来的、关于 Python、也关于如何辨别表象与本质的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写比较时,先顿一下问问自己"我是要比值还是比身份"、把比值的 is 换成 ==,那我对着那个"1000 == 1000 是 True、1000 is 1000 却是 False"的诡异现场百思不解的这段时间,就值了。
—— 别看了 · 2026