← 返回题目列表

Python 中 hashable 和 unhashable 是什么意思?

高频 中等 第 11 / 21 题 更新于 2026/07/25
Pythonhashable哈希dict key

简化版

hashable 表示对象在生命周期内有稳定哈希值,并且可以用于 dict key 或 set 元素。不可变类型通常可哈希,例如 intstr、只包含可哈希元素的 tuple;可变类型通常不可哈希,例如 listdictset

详细版

可以作为 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

完整版教学

一、可哈希需要满足什么条件

可哈希对象要满足两个核心要求:

  1. 能返回哈希值。
  2. 哈希值在对象生命周期内保持稳定。

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__