一个让我熬夜到凌晨三点的Bug
先跟你说个真事儿。
上周五晚上,我写了一段用户登录的代码。逻辑很简单------从数据库查出用户信息,判断是不是管理员,决定跳转到哪个后台页面。
代码长这样:
scss
user = get_user_from_db(user_id) # 返回一个字典
if user['role'] is 'admin':
redirect_to_admin_page()
else:
redirect_to_user_page()
你猜怎么着?所有管理员登录后都跳转到了普通用户页面。
我盯着屏幕看了半小时,数据库里role字段明明白白写着'admin',打印出来也是'admin',可if条件就是进不去。
后来把is换成==,一切正常了。
就这一个字符的区别,我熬到凌晨三点。
从那以后我才真正搞明白,Python里is和==根本不是一回事。今天我就把这次踩坑的教训好好跟你说说。
先记住一句话
在深入之前,先记住这句核心区别:
==比较的是值(内容),is比较的是身份(内存地址)。
用大白话讲:
==问的是:这两个东西长得一不一样?is问的是:这两个东西是不是同一个东西?
长得一样的东西,不一定是同一个东西。就像双胞胎长得一模一样,但他们是两个人。==看长相,is看身份证号。
Python里每个对象都有三个基本属性:id(身份标识) 、type(数据类型) 和value(值) 。is比的是id,==比的是value。
场景一:列表------最直观的对比
先看一段代码:
ini
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a == b) # True
print(a is b) # True
print(a == c) # True
print(a is c) # False
同样都是[1, 2, 3],为什么a is b是True,a is c却是False?
因为b = a这句,b只是给a取了个别名 ,a和b指向的是内存里同一个列表对象。
而c = [1, 2, 3]是新创建 了一个列表。虽然内容和a一模一样,但它们是内存里两个不同的对象。
你可以用id()函数验证一下:
bash
print(id(a)) # 比如 4364243328
print(id(b)) # 4364243328,和a一样
print(id(c)) # 4364202696,不一样
这就是为什么a is c是False------它们的"身份证号"不同。
场景二:整数------那个把我骗得最惨的坑
整数比较是新手最容易翻车的地方。
看这个:
ini
a = 256
b = 256
print(a is b) # True
c = 257
d = 257
print(c is d) # False
同样是两个相等的整数,256返回True,257返回False。
这什么逻辑?
因为Python有个"小整数对象池" 。
Python启动的时候,会把-5到256之间的整数提前创建好,放在一个池子里。以后你用这个范围内的任何数字,Python都直接从池子里拿,不会新建。
所以a = 256和b = 256,两个变量拿到的其实是同一个整数对象。
但257不在池子里,每次赋值都会新创建一个对象 。所以c is d是False------值一样,但不是同一个东西。
这不就坑了吗? 我今天用is比较100没问题,明天换成1000就出bug了。这种不确定性,谁受得了?
场景三:字符串------时灵时不灵
字符串也有类似的坑。
ini
s1 = "hello"
s2 = "hello"
print(s1 is s2) # 通常是 True
但换个写法:
ini
s1 = "hello world!"
s2 = "hello world!"
print(s1 is s2) # 可能是 False
Python对字符串也有"驻留"机制------某些简单的字符串会被缓存复用。但规则很复杂,跟字符串长度、内容都有关系。
你永远不确定is会不会返回True 。所以比较字符串值的时候,永远用==。
场景四:自定义类------又一个隐形陷阱
自己定义的类,==的行为也不一样。
ini
class Person:
def __init__(self, name):
self.name = name
p1 = Person("张三")
p2 = Person("张三")
print(p1 == p2) # False
print(p1 is p2) # False
两个Person对象,名字一样,但==和is都返回False。
因为默认情况下,自定义类的==比较的也是内存地址 ,和is没有区别。
如果想让==比较内容,需要自己实现__eq__方法:
python
class Person:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
p1 = Person("张三")
p2 = Person("张三")
print(p1 == p2) # True
print(p1 is p2) # False
到底什么时候用is?
说了这么多坑,那is是不是就没用了?
当然不是。is有它专属的使用场景:
1. 判断None
这是Python官方推荐的做法:
python
# ✅ 正确
if data is None:
print("数据为空")
# ❌ 不推荐
if data == None:
print("数据为空")
因为None在Python里是单例 ------整个程序里只有一个None对象。用is判断既准确又高效。
2. 判断单例
类似的,True、False也是单例,也可以用is。
3. 检查两个变量是否指向同一个对象
比如你想确认两个变量是不是引用了同一个列表/字典实例。
除了这些情况,比较值请一律用==。
为什么is比==快?
顺便说个冷知识:is比==快。
因为is只是比较两个内存地址是否相同,就是一个整数比较。而==要调用对象的__eq__()方法,可能涉及复杂的值比较逻辑。
而且is不能被重载 ,行为是固定的。==可以被重载,不同类的==行为可能不一样。
但别为了这点性能就用is比较值------正确性永远比性能重要。
总结一下
| 运算符 | 比较什么 | 通俗理解 | 使用场景 |
|---|---|---|---|
== |
值是否相等 | 长得像不像 | 绝大多数情况 |
is |
是否是同一个对象 | 是不是同一个人 | 判断None、单例、对象身份 |
记住这几个原则:
- 比较值,永远用
==------不管是数字、字符串、列表还是字典 - 判断
None,用is------这是Python社区的共识 - 别拿
is比较整数和字符串------小整数池和字符串驻留会让结果时灵时不灵 - 自定义类要实现
__eq__------不然==和is没区别
那次熬夜之后,我给自己定了个规矩:除了判断None,一律用==。
如果你现在代码里还在用is比较字符串或者大整数,赶紧去改了吧。别等到凌晨三点才发现问题。