一个让我熬夜到凌晨三点的 Bug
先说说我是怎么栽在这个坑里的。
去年有个项目,我要写一个缓存系统。逻辑很简单:从数据库里查出用户 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 问你俩是不是同一个人**。
看几个最简单的例子:
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。这些整数是"常驻内存"的,不管你写多少遍,用的都是同一个对象。
这就是所谓的 **"小整数池"**。
所以当你写 a = 100 和 b = 100 时,Python 不会新建两个整数对象,而是直接把两个变量指向小整数池里的那个 100。所以 a is b 是 True——它们确实是同一个对象。
但当你写 a = 1000 时,1000 不在小整数池里,Python 会新建一个整数对象。再写 b = 1000,又新建一个。虽然值一样,但两个对象在内存里是不同的,所以 a is b 是 False。
这就是为什么你平时用 is 比较小整数一直没问题,一到生产环境数据量大了、数字超过 256 就莫名失灵的根本原因。
同样的道理也适用于字符串。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 的,而且应该用 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"。
一个更隐蔽的坑:自定义类的实例
这个问题在自定义类里尤其容易踩。
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):
if not isinstance(other, Person):
return False
return self.name == other.name
这时候 p1 == p2 就是 True 了,但 p1 is p2 仍然是 False——因为它们是两个不同的对象,只是名字一样而已。
这就是 == 和 is 的本质区别:== 的行为是可以自定义的,而 is 是内存地址的直接比较,永远无法改变。
什么时候用 ==,什么时候用 is?
经过那次三个小时的教训,我给自己定了一个铁律:
比较值的时候,永远用 ==,除非你有 100% 的把握要用 is。
什么时候可以有那 100% 的把握?只有这三种情况:
- 判断
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 和 == 的区别,写出来就是一句话,但真正理解它需要踩一次坑。
== 问的是"你俩值一样吗",is 问的是"你俩是同一个东西吗"。大多数时候你想问的都是前者,而不是后者。
小整数池和字符串驻留这些优化机制,本来是 Python 为了提升性能做的好事,但它们也让 is 的行为变得"时灵时不灵",反而成了陷阱的源头。
记住一句话:除非比较 None,否则默认用 ==。 这个习惯能帮你省下无数个深夜调试的时间。
希望你不用像我一样,等到凌晨三点才明白这个道理。