Python 的 is 把我坑惨了,原来 == 和 is 在小整数池外完全是两码事

一个让我熬夜到凌晨三点的 Bug

先说说我是怎么栽在这个坑里的。

去年有个项目,我要写一个缓存系统。逻辑很简单:从数据库里查出用户 ID,然后判断这个 ID 是不是 0------0 代表"游客",需要特殊处理。

代码大概是这样的:

python 复制代码
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_idnumpy.int64(0),不是 Python 内置的 int 类型。

numpy.int64(0) is 0 返回 False,而 numpy.int64(0) == 0 返回 True

那一刻我意识到:我一直在用 is 做值比较,但这个操作符根本不是干这个用的。

改了一行代码------把 is 换成 ==------问题解决了。但那天晚上的三个小时再也回不来了。

== 和 is 到底有什么不一样?

先给出最简洁的答案:

  • == 比较的是值------两个东西"长得是不是一样"
  • is 比较的是身份------两个东西"是不是同一个东西"

用大白话说:== 问你俩是不是双胞胎,is 问你俩是不是同一个人

看几个最简单的例子:

css 复制代码
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 就不行了?

先看这段代码:

ini 复制代码
a = 100
b = 100
print(a is b)  # True

再看这段:

ini 复制代码
a = 1000
b = 1000
print(a is b)  # False

同样的写法,只是数字从 100 换成了 1000,结果完全不一样。

你是不是觉得 Python 疯了?

其实 Python 没疯,它只是在背后做了一件优化的事。

Python 在启动的时候,会预先创建好一批整数对象,范围是 -5 到 256。这些整数是"常驻内存"的,不管你写多少遍,用的都是同一个对象。

这就是所谓的 "小整数池"

所以当你写 a = 100b = 100 时,Python 不会新建两个整数对象,而是直接把两个变量指向小整数池里的那个 100。所以 a is bTrue------它们确实是同一个对象。

但当你写 a = 1000 时,1000 不在小整数池里,Python 会新建一个整数对象。再写 b = 1000,又新建一个。虽然值一样,但两个对象在内存里是不同的,所以 a is bFalse

这就是为什么你平时用 is 比较小整数一直没问题,一到生产环境数据量大了、数字超过 256 就莫名失灵的根本原因。

同样的道理也适用于字符串。Python 有一种叫 "字符串驻留" 的机制------代码里写死的字符串字面量,比如 "hello",会被复用同一个对象。但动态生成的字符串,比如 "".join(["h", "e", "l", "l", "o"]),就是新建的对象。

ini 复制代码
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------就是那些全局唯一的单例

  • None
  • True
  • False

这几个对象在 Python 整个运行过程中有且只有一个实例。不管你怎么引用,它们都是同一个对象。

所以判断一个变量是不是 None,标准写法是:

python 复制代码
if x is None:   # 正确
if x == None:   # 不推荐,虽然能工作但语义不对

判断布尔值也是同理:

ini 复制代码
if flag is True:    # 可以,但通常直接 if flag: 更简洁
if flag == True:    # 不推荐

为什么官方推荐用 is 来比较 None?因为 None 是单例,用 is 比较的是身份,速度更快,而且语义更准确------你确实想知道"这个变量是不是那个唯一的 None",而不是"这个变量的值是否等于 None"。

一个更隐蔽的坑:自定义类的实例

这个问题在自定义类里尤其容易踩。

ini 复制代码
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__ 方法:

ruby 复制代码
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% 的把握?只有这三种情况:

  • 判断 Nonex is None
  • 判断 True / Falsex is True(但通常直接用 if x:if not x:
  • 判断是否是同一个对象(比如在缓存里判断两个引用是否指向同一个实例)

除此之外,一律用 ==

不管是你觉得"这个数字肯定很小不会超 256",还是"这个字符串肯定是字面量",都不要赌。生产环境的数据你永远猜不到它会变成什么样。

csharp 复制代码
# 永远这样做
if user_id == 0:
    return "游客"

# 不要这样做
if user_id is 0:   # 万一 user_id 不是 int 呢?万一超过 256 呢?
    return "游客"

写在最后

那天凌晨三点,我把 is 改成 == 之后,缓存系统恢复了正常。关掉 IDE 的时候我在想------如果当初写代码的时候能多想一想"我到底要比较什么",那三个小时完全不用浪费。

is== 的区别,写出来就是一句话,但真正理解它需要踩一次坑。

== 问的是"你俩值一样吗",is 问的是"你俩是同一个东西吗"。大多数时候你想问的都是前者,而不是后者。

小整数池和字符串驻留这些优化机制,本来是 Python 为了提升性能做的好事,但它们也让 is 的行为变得"时灵时不灵",反而成了陷阱的源头。

记住一句话:除非比较 None,否则默认用 == 这个习惯能帮你省下无数个深夜调试的时间。

希望你不用像我一样,等到凌晨三点才明白这个道理。

相关推荐
用户40966601317511 小时前
Jackson 序列化:@JsonIgnore / @JsonProperty / @JsonFormat / @JsonInclude / @JsonUnwrapped 一次讲清楚
java·后端
雨落倾城夏未凉1 小时前
halcon核心-图像预处理(四)
后端
用户233376852182 小时前
一张损坏JPEG文件的背后排查
后端
nnerddboy2 小时前
Rust教程06:ESP32-rust环境搭建
开发语言·后端·rust
步行cgn2 小时前
MyBatis resultMap 结果映射完全指南
后端
nnerddboy2 小时前
Rust教程03:函数,控制流与所有权
开发语言·后端·rust
山荷枝2 小时前
04-框架--SpringBoot
java·spring boot·后端
Undoom3 小时前
用TextIn xParse搞定批量简历解析,WorkBuddy里一句话就能完成
后端
乐橙开放平台4 小时前
一周上线校园透明化:listDeviceDetailsByPage 台账 + getKitToken + ImouPlayer 多路墙
后端·物联网·安全·音视频·智能家居