Python 对象的 ==、is 和 hash 有什么关系?
简化版
is 比较两个变量是否指向同一个对象,== 比较对象是否值相等,具体逻辑可由 __eq__ 定义。可哈希对象必须保证相等对象哈希值相等,所以重写 __eq__ 时要认真考虑 __hash__。
详细版
示例:
a = [1, 2]
b = [1, 2]
print(a == b) # True
print(a is b) # False
两个列表内容相等,但不是同一个对象。
自定义对象默认比较身份:
class User:
def __init__(self, user_id):
self.user_id = user_id
u1 = User(1)
u2 = User(1)
print(u1 == u2) # False
如果希望按字段比较,需要实现 __eq__:
class User:
def __init__(self, user_id):
self.user_id = user_id
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return self.user_id == other.user_id
哈希规则:
如果 a == b,那么 hash(a) 必须等于 hash(b)
如果对象可变,通常不适合作为字典 key 或 set 元素。
完整版教学
一、is 比较身份
is 判断两个引用是不是指向同一个对象。
a = []
b = a
c = []
print(a is b) # True
print(a is c) # False
a 和 b 指向同一个列表;c 是另一个新列表。
常见正确用法:
if value is None:
...
判断 None 时应该用 is,因为 None 是单例对象。
二、== 比较值相等
== 会调用对象的相等比较逻辑。
[1, 2] == [1, 2]
列表实现了按元素比较,所以结果是 True。
自定义类如果不实现 __eq__,默认行为通常接近身份比较。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
Point(1, 2) == Point(1, 2) # False
要按值比较,就实现 __eq__。
三、eq 的返回策略
推荐写法:
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
返回 NotImplemented 表示当前比较逻辑不支持对方类型,让 Python 尝试其他路径。
如果直接对陌生类型返回 False,在某些反向比较场景下不够友好。
四、hash 用在哪里
哈希用于字典和集合:
d = {}
d[key] = value
s = {item}
字典和集合需要根据哈希值快速定位元素。
所以作为 key 的对象必须可哈希,并且哈希值在生命周期内应该稳定。
可变列表不能做 key:
{[1, 2]: "value"} # TypeError
元组如果内部都是可哈希元素,通常可以做 key:
{(1, 2): "value"}
五、eq 和 hash 要一致
如果:
a == b
那么必须满足:
hash(a) == hash(b)
否则字典和集合会出现逻辑混乱。
例如:
class User:
def __init__(self, user_id):
self.user_id = user_id
def __eq__(self, other):
return isinstance(other, User) and self.user_id == other.user_id
def __hash__(self):
return hash(self.user_id)
如果 user_id 创建后不再变化,这样是合理的。
六、可变对象为什么不适合哈希
如果对象放进 set 后,参与哈希的字段变了,集合就可能再也找不到它。
u = User(1)
s = {u}
u.user_id = 2
如果 __hash__ 依赖 user_id,修改后哈希位置变了,但集合内部结构没有跟着重排。
因此可哈希对象最好是不可变值对象。可变业务对象一般不要自定义可变字段哈希。
七、常见误区与追问
记忆钩子:
is看身份,==看相等语义,hash看能否稳定放进哈希表。三者不是一回事,但==和hash必须遵守同一个相等契约。
- 误区:
is和==只是写法不同。is比较两个名字是否指向同一个对象,==会调用相等比较逻辑;两个内容相同的列表==可以为真,但is通常为假。 - 追问:为什么相等对象必须哈希相等? 字典和集合先用哈希定位桶,再用相等判断确认对象;如果
a == b但hash(a) != hash(b),同一个逻辑 key 可能落到不同位置。 - 误区:重写
__eq__后不用管__hash__。 Python 会把这类对象默认变成不可哈希,除非你显式提供与相等逻辑一致的__hash__,这是为了保护 dict/set 的正确性。 - 追问:为什么列表不可哈希但元组有时可哈希? 列表可变,哈希不稳定;元组不可变,但只有当元组内部所有元素都可哈希时,元组整体才可哈希。
- 误区:哈希值唯一代表对象。 哈希可能冲突,dict/set 仍需要用
__eq__做最终确认;哈希的目标是快速缩小查找范围,不是提供全局唯一 ID。 - 追问:业务对象按 id 相等是否合理? 如果业务语义认为同一个
user_id就是同一用户,可以按 id 实现__eq__和__hash__;前提是参与哈希的 id 在对象生命周期内保持稳定。
八、加强记忆
is 看是不是同一个对象,== 看值是否相等,hash 服务字典和集合。重写 __eq__ 就要想清楚 __hash__:相等对象必须哈希相等,参与哈希的字段最好不可变。面试里用列表的 == 和 is 对比例子最容易讲明白。