17370845950

如何让 hash 只在 frozen dataclass 上生效
普通dataclass的hash为False,因为Python默认生成的__hash__为None;即使设hash=True,含可变字段(如list)时也会被静默忽略,因哈希值需在对象生命周期内恒定。

为什么普通 dataclass 的 hash 是 False

Python 默认给 @dataclass 生成的 __hash__ 方法是 None,哪怕你显式写 hash=True,只要类里有可变字段(比如 listdict),解释器就会静默忽略它,最终实例仍是不可哈希的。根本原因在于:Python 要求哈希值在对象生命周期内恒定,而可变字段的内容随时可能变。

frozen=True 是必要条件,但还不够

frozen=True 会让 dataclass 把所有字段设为只读(背后调用 object.__setattr__ 拦截赋值),这是启用 hash 的前提。但仅加 frozen=True 不等于自动有 hash —— 你必须**同时指定 hash=None 或明确写 hash=True**,否则 Python 仍按默认逻辑推导(即:有可变字段 → 禁用 hash)。

  • ✅ 正确:@dataclass(frozen=True, hash=True)
  • ✅ 也行:@dataclass(frozen=True, hash=None)(此时会按字段类型自动推导 hash 行为)
  • ❌ 错误:@dataclass(frozen=True)hash 未显式设置,且字段含 list 等 → __hash__ = None

字段类型决定 hash 是否真能用

即使加了 frozen=Truehash=True,如果某个字段本身不可哈希(例如 listdict、自定义类没实现 __hash__),实例创建时不会报错,但调用 hash() 会立刻抛 TypeError: unhashable type

常见修复方式:

  • list 改成 tupletuple 可哈希,前提是元素也都可哈希)
  • dict 改成 frozensettuple(sorted(dict.items()))
  • 对自定义类,确保它有确定的 __hash__ 且不依赖可变状态

示例:

@dataclass(frozen=True, hash=True)
class Point:
    x: int
    y: int
    tags: tuple[str, ...]  # ✅ 可哈希

❌ 这样会 runtime 报错:hash(Point(1, 2, ["a"])) → TypeError

tags: list[str]

嵌套 dataclass 的 hash 传递性

如果 frozen dataclass 的某个字段是另一个 frozen dataclass 实例,那它的 hash 值会被递归纳入计算 —— 但前提是那个嵌套类也满足 frozen=True 且所有字段可哈希。一旦其中任意一层出现不可哈希字段,整个链路就失效。

容易忽略的点:

  • field(default_factory=list) 即使在 frozen 类里也不行 —— default_factory 返回的仍是可变对象
  • InitVar 字段不参与 hash 计算,但若它被用来初始化一个不可哈希的字段,问题照旧
  • 使用 field(compare=False) 的字段仍参与 hash,除非你也设 hash=False

真正安全的初始化写法是:所有字段类型本身可哈希,且不依赖运行时动态构造的可变容器。