一个让我熬夜到凌晨三点的 Bug
先说说我是怎么掉进这个 Python 坑里的。
去年做一个项目时,我需要实现一套缓存系统。整体逻辑并不复杂:先从数据库中取出用户 ID,再判断这个 ID 是否为 0——0 代表"游客",需要走单独的处理流程。
代码大概是这样的:
def get_user_role(user_id):
# user_id 是从数据库查出来的
if user_id is 0:
return "游客"
return "注册用户"
本地测试时一切正常,user_id 为 0 时也能正确返回"游客"。于是我很放心地把代码部署到了线上环境。
结果上线后,监控系统开始持续报警——"游客"身份的判断偶尔会失效,有些明明是 0 的用户,却被系统识别成了注册用户。
我盯着这段代码排查了两个小时,确认 user_id 的值确实是数据库返回的 0。按我当时的理解,Python 里 0 is 0 不是应该得到 True 吗?本地环境也明明跑得没问题。
后来我把 user_id 的类型打印出来才发现——数据库驱动返回的 user_id 实际上是 numpy.int64(0),并不是 Python 内置的 int 类型。
numpy.int64(0) is 0 返回 False,而 numpy.int64(0) == 0 返回 True。
那一刻我才真正意识到:我一直在拿 is 做值比较,但这个运算符从设计上就不是用来比较值的。

最后我只改了一行代码——把 is 换成 ==——问题立刻解决了。但那天晚上浪费掉的三个小时,是真的回不来了。
== 和 is 到底有什么不一样?
先给出最直接、也最适合记忆的答案:
== 比较的是值——两个对象"内容是否相等"is 比较的是身份——两个变量"是否引用同一个对象"用更通俗的话说:**== 问的是你们像不像,is 问的是你们是不是同一个。**
先看几个最基础的 Python 示例:
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True,两个列表的值完全一样
print(a is b) # False,两个列表是不同的对象,只是内容相同
print(a is c) # True,a 和 c 引用的是同一个对象
这个例子不难理解:一个是重新创建的新列表,一个只是变量引用传递。内容可以一样,但对象本身未必相同。
真正容易让人混淆的是——对于数字、字符串这类基础数据类型,Python 在底层做了一些优化,于是很多人会误以为 is 也能拿来比较值。
小整数池:为什么 256 以内 is 没问题,257 就不行了?
先看这段代码:
a = 100
b = 100
print(a is b) # True
再看这段:
a = 1000
b = 1000
print(a is b) # False
同样的写法,只是把数字从 100 换成了 1000,结果却完全不同。
你可能会觉得这是 Python 的"迷惑行为"。
其实 Python 并没有出问题,它只是悄悄做了性能优化。
在 Python 启动时,会提前创建一批常用整数对象,范围通常是 -5 到 256。这些整数会被缓存并长期复用。
这就是很多 Python 开发者常说的 **"小整数池"**。
所以当你写 a = 100 和 b = 100 时,Python 往往不会创建两个新的整数对象,而是让两个变量都指向小整数池中的那个 100。因此 a is b 才会是 True——因为它们确实引用的是同一个对象。
但当你写 a = 1000 时,1000 通常不在小整数池内,Python 就可能新建一个整数对象;再写 b = 1000 时,也可能再创建一个新的对象。虽然数值相同,但内存中的对象不同,所以 a is b 会得到 False。
这也正是为什么你平时用 is 比较小整数看起来"一直没问题",可一到真实业务场景、线上环境或数据类型稍有变化时,就突然失效了。
同样的逻辑也适用于字符串。Python 还有一种叫 "字符串驻留" 的优化机制——代码中直接写出的字符串字面量,例如 "hello",常常会复用同一个对象;但运行时动态拼接出来的字符串,例如 "".join(["h", "e", "l", "l", "o"]),通常会创建新的对象。
a = "hello"
b = "hello"
print(a is b) # True,两个字面量通常会复用同一个对象
a = "".join(["h", "e", "l", "l", "o"])
b = "".join(["h", "e", "l", "l", "o"])
print(a is b) # False,这里是运行时分别创建出的两个对象
也就是说,你用 is 比较字符串时,有时结果正确,有时结果错误,完全取决于这个字符串是字面量还是动态生成的。把这样的行为用在业务判断里,风险非常大。
None、True、False:这几个是例外
有一类对象是适合使用 is 的,而且从 Python 编码规范来看,本来就应该用 is——它们就是全局唯一的单例对象。
NoneTrueFalse这几个对象在 Python 程序运行期间通常都只有一个实例。不管你在什么地方引用,它们本质上都是同一个对象。
所以判断一个变量是不是 None,推荐的标准写法是:
if x is None: # 正确
if x == None: # 不推荐,虽然通常能运行,但语义不准确
判断布尔值时也是类似的:
if flag is True: # 可以,但通常直接 if flag: 更简洁
if flag == True: # 不推荐
为什么官方更推荐用 is 比较 None?因为 None 是单例对象,用 is 判断身份更符合语义,也更直接——你真正想确认的是"这个变量是不是那个唯一的 None",而不是"它的值是否等于 None"。
一个更隐蔽的坑:自定义类的实例
在自定义类中,== 和 is 的区别会更加明显,也更容易让初学者踩坑。
class Person:
def __init__(self, name):
self.name = name
p1 = Person("张三")
p2 = Person("张三")
print(p1 == p2) # False,默认比较的仍然是对象本身
print(p1 is p2) # False,本来就是两个不同的实例
如果你希望 p1 == p2 返回 True,就需要在类中实现 __eq__ 方法:
class Person:
def __init__(self, name):
self.name = name
def __eq__(self, other):
先判断 other 是不是 Person 实例,
if not isinstance(other, Person):
return False
再比较两者的 name 是否一致,
return self.name == other.name
这样一来,p1 == p2 就会变成 True,但 p1 is p2 依然是 False——因为它们终究是两个不同的对象,只是属性值相同。
这正是 == 和 is 的根本差异:== 的比较逻辑可以由对象自行定义,而 is 比较的是对象身份,本质上不会被重写,也不会因为你定义了类而改变。
什么时候用 ==,什么时候用 is?
经历了那次排查到凌晨三点的线上 Bug 之后,我给自己定下了一条很实用的 Python 编码原则:
只要是在比较值,默认永远用 ==;除非你 100% 确定自己要比较的是对象身份,才使用 is。
那什么时候才算真的可以放心用 is?通常只有这几种情况:
None:x is None判断 True / False:x is True(但大多数时候直接用 if x: 或 if not x: 更自然)判断两个变量是否引用同一个对象(例如缓存场景中判断两个引用是否指向同一个实例)除了这些场景之外,**基本都应该优先使用 ==**。
不管你心里觉得"这个数字肯定不会超过 256",还是"这个字符串看起来一定是字面量",都不要拿业务代码去赌。真实生产环境里的输入数据、第三方库返回值、数据库驱动类型,都可能和你的想象完全不同。
# 永远这样做
if user_id == 0:
return "游客"
# 不要这样做
if user_id is 0: # 万一 user_id 不是 int 呢?万一超过 256 呢?
return "游客"
写在最后
那天凌晨三点,当我把 is 改成 == 之后,整个缓存系统终于恢复正常。关掉 IDE 的那一刻,我脑子里只剩下一个念头——如果当时写代码时能多问自己一句"我比较的到底是值,还是对象本身",那三个小时完全可以省下来。
is 和 == 的区别,写成定义其实只有一句话,但真正理解它,往往要经历一次真实的 Bug 才会印象深刻。
== 问的是"你们的值是否相等",is 问的是"你们是不是同一个对象"。而在绝大多数 Python 开发场景里,你真正想判断的,几乎都是前者。
小整数池、字符串驻留这些底层优化机制,本意都是为了提升 Python 性能,但也正因为它们的存在,is 在某些场景下会显得"偶尔可用、偶尔失灵",从而成为很多线上问题的根源。
记住一句很实用的话:除非是在比较 None 或明确判断对象身份,否则默认使用 ==。养成这个习惯,能帮你减少大量低级错误,也能省下无数个深夜排查 Bug 的时间。
希望你不用像我一样,等到凌晨三点,才彻底搞懂这个道理。
