Python 中 hashable 和 unhashable 是什么意思?
简化版
hashable 表示对象在生命周期内有稳定哈希值,并且可以用于 dict key 或 set 元素。不可变类型通常可哈希,例如 int、str、只包含可哈希元素的 tuple;可变类型通常不可哈希,例如 list、dict、set。
详细版
可以作为 dict key:
d = {}
d[1] = "int"
d["name"] = "str"
d[(1, 2)] = "tuple"
不可以:
# d[[1, 2]] = "list" # TypeError: unhashable type: 'list'
# d[{"a": 1}] = "dict"
tuple 要特别注意:
hash((1, 2, "a")) # 可以
# hash(([1, 2], "a")) # TypeError
因为第二个 tuple 内部包含 list,list 不可哈希,所以整个 tuple 也不可哈希。
判断对象是否可哈希可以用:
from collections.abc import Hashable
isinstance("abc", Hashable) # True
isinstance([], Hashable) # False
完整版教学
一、可哈希需要满足什么条件
可哈希对象要满足两个核心要求:
- 能返回哈希值。
- 哈希值在对象生命周期内保持稳定。
dict 和 set 依赖哈希值定位元素。如果一个 key 插入后哈希值变了,容器就可能找不到它。
这就是为什么可变对象通常不能做 key。
items = [1, 2]
# hash(items) # TypeError
列表内容随时能变,哈希值无法安全稳定。
可以把哈希理解成“把 key 映射到哈希表位置的摘要”。dict/set 先用哈希值缩小查找范围,再用相等性确认是不是同一个 key。稳定性非常重要:插入时算出来在第 5 号位置,查找时如果变成第 17 号位置,容器就乱了。
key -> hash(key) -> 桶位 -> 比较是否相等 -> 找到/插入
二、不可变对象是不是一定可哈希
大多数常见不可变对象可哈希:
hash(1)
hash("python")
hash((1, 2))
但 tuple 是一个容易误判的例子。tuple 本身不可变,但如果它内部引用了不可哈希对象,整体就不可哈希:
t = ([1, 2], 3)
# hash(t) # TypeError
因为内部列表可变,tuple 的“整体值”也会受到列表内容变化影响。
更准确地说,“不可变”是可哈希的常见条件,但不是口头上的绝对充分条件。tuple 的哈希需要递归依赖内部元素;frozenset 也要求内部元素可哈希。另一方面,普通自定义对象即使内部属性可变,默认也可能按身份哈希,但如果你把它放进 set 后又改变参与 __eq__ 的字段,就会制造逻辑问题。
| 对象 | 是否常见可哈希 | 原因 |
|---|---|---|
1、"a" | 是 | 不可变且哈希稳定 |
(1, "a") | 是 | 内部元素都可哈希 |
([1], "a") | 否 | 内部 list 不可哈希 |
[1, 2] | 否 | 可变 |
frozenset({1, 2}) | 是 | 不可变集合且元素可哈希 |
三、相等对象必须有相等哈希
哈希和相等性之间有重要约束:
如果 a == b,那么 hash(a) 必须等于 hash(b)
但反过来不成立:
hash(a) == hash(b) 不一定代表 a == b
不同对象可能产生相同哈希值,这叫哈希冲突。dict 和 set 必须能处理冲突。
数字例子:1 == 1.0 == True 在 Python 中有特殊相等关系,它们的哈希也相等,所以放进 dict 可能互相覆盖。这个例子能说明“哈希”和“相等性”必须配套。
print(1 == 1.0 == True) # True
print(hash(1), hash(1.0), hash(True)) # 通常都是 1
反过来,哈希相等不保证对象相等,因为哈希空间有限而对象集合无限。发生冲突时,dict/set 会继续用 == 判断是不是同一个 key。
四、自定义对象和 __hash__
默认情况下,普通自定义对象通常按身份比较和哈希:
class User:
pass
u1 = User()
u2 = User()
print(u1 == u2) # False
如果你重写 __eq__,Python 会谨慎处理 __hash__,避免“相等逻辑变了但哈希逻辑没变”的错误。
例如:
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
这类对象默认可能变得不可哈希。要让它安全可哈希,需要保证参与相等和哈希计算的字段不可变,并正确实现 __hash__。
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 元素,随后把 user_id 从 1 改成 2,它在 set 里的位置仍按旧哈希放着,查找语义就会变得危险。更稳的做法是让用于哈希的字段不可变,或者用不可变数据类。
易错点:重写
__eq__后不要随手补一个__hash__了事;先确认参与相等比较的字段是否真的不可变。
五、实际开发中的常见场景
高频场景:
- 用 tuple 表示坐标 key:
visited.add((x, y))。 - 用 frozenset 表示无序组合 key。
- 用 dict 统计频率:key 必须可哈希。
- 避免把 list 直接放进 set,需要转成 tuple。
例如二维坐标去重:
visited = set()
visited.add((3, 5))
不能写:
# visited.add([3, 5])
无序组合 key 可以用 frozenset,比如“用户 A 和用户 B 的关系”不关心顺序,frozenset({a, b}) 比 (min(a,b), max(a,b)) 更直观。可变列表想进 set 时,通常先转 tuple;但要知道这只是固定外层序列,如果里面还有 list,仍然不可哈希。
edge = frozenset({"u1", "u2"})
relations = {edge: "friend"}
六、Hashable 判断和运行时错误
可以用 collections.abc.Hashable 判断对象是否声明为可哈希,也可以直接调用 hash()。实际写代码时,很多错误是在插入 dict/set 时暴露的:TypeError: unhashable type: 'list'。这不是 list 不能比较相等,而是 list 不适合作为哈希表 key。
from collections.abc import Hashable
print(isinstance((1, 2), Hashable)) # True
print(isinstance(([1], 2), Hashable)) # True,注意这可能只看类型层面
# hash(([1], 2)) # TypeError,实际哈希时失败
这个细节说明:判断复杂嵌套对象是否真的能 hash,直接 hash(obj) 更可靠。Hashable 对某些容器只能说明类型提供了哈希入口,内部元素不合格时仍会在计算时失败。
七、常见误区与追问
- 误区:不可变对象一定可哈希。 tuple、frozenset 还要看内部元素是否可哈希,嵌套 list 会导致失败。
- 误区:hash 相等就说明对象相等。 哈希冲突允许存在,最终还要用
==判断 key 是否相等。 - 误区:自定义对象默认都适合做 key。 默认身份哈希可以用,但一旦重写
__eq__,就必须重新审视__hash__和字段稳定性。 - 追问:为什么
a == b时要求hash(a) == hash(b)? dict/set 先按哈希定位,如果相等对象哈希不同,就可能被放到不同位置,查找会错。 - 追问:list 为什么 unhashable? list 内容可变,哈希和相等性无法在生命周期内保持稳定。
- 追问:二维坐标去重为什么常用 tuple?
(x, y)不可变且元素可哈希,适合作为 set 元素或 dict key。
八、加强记忆
把 hashable 记成“能稳定进哈希表”:对象要能算 hash,且参与相等比较的状态不能乱变;相等对象必须哈希相等,哈希相等不一定对象相等。常见可哈希有 int、str、只含可哈希元素的 tuple、frozenset;常见不可哈希有 list、dict、set。自定义对象只要碰到 __eq__,就必须同时考虑 __hash__。