哈希(Hashing)

哈希方法生成(Hash Method Generation)

警告

总体主题是切勿自己设置 @attrs.define(unsafe_hash=X) 参数。
将其保留为 None,这意味着 attrs 会根据其他参数为您做正确的事情:

  • 如果您希望通过值使对象可哈希:使用 @define(frozen=True)

  • 如果您希望通过对象身份进行哈希和相等性比较:使用 @define(eq=False)

自己设置 unsafe_hash 可能会产生意想不到的后果,因此我们建议您仅在确切知道自己在做什么的情况下进行调整。

在某些情况下,对象需要是 可哈希 的。
例如,如果您想将它们放入一个 set 或者想将它们用作 dict 中的键。

对象的 哈希 是一个表示对象内容的整数。
可以通过在对象上调用 hash() 来获得,并通过为您的类编写 __hash__ 方法来实现。

attrs 会很乐意为您编写 __hash__ 方法 [1],但默认情况下不会这样做。
因为根据官方 Python 文档的 定义,返回的哈希必须满足某些约束条件:

  1. 两个相等的对象 必须 具有相同的哈希。
    这意味着如果 x == y,那么 必须 确保 hash(x) == hash(y)

    默认情况下,Python 类是通过其 id 进行比较和哈希的。
    这意味着类的每个实例都有不同的哈希,无论它携带哪些属性。

    由此可见,当您(或 attrs)通过基于属性值实现的 __eq__ 更改相等性处理方式时,这一约束就会被打破。
    因此,Python 3 会使具有自定义相等性的类变为不可哈希。
    而 Python 2 则乐于让您自食其果。
    不幸的是,如果您设置 unsafe_hash=Falseattrs 仍然模仿(不再支持)Python 2 的行为,以保持向后兼容性。

    实现基于 ID 的哈希的 正确方法 是设置 @define(eq=False)
    设置 @define(unsafe_hash=False)(这隐含 eq=True)几乎肯定是一个 bug

    警告

    小心子类化!
    在具有非默认 __hash__ 方法的基类上将 eq=False 设置为 不会 使 attrs 为您删除该 __hash__

    attrs 的理念是仅对类进行 添加,因此您可以自由自定义您的类。
    如果您想 移除 方法,则必须手动完成。

    在类体中添加 __hash__ = object.__hash__ 是重置 __hash__ 的最简单方法。

  2. 如果两个对象不相等,它们的哈希 应该 不同。

    虽然从正确性角度来看这不是强制要求,但如果有许多相同的哈希,集合和字典的效率会降低。
    最糟糕的情况是,当所有对象具有相同的哈希,这会使集合变成列表。

  3. 对象的哈希 必须不 改变。

    如果您创建一个使用 @define(frozen=True) 的类,这一点在定义上得到了满足,因此 attrs 会自动为您编写 __hash__ 函数。
    您也可以通过 unsafe_hash=True 强制它为您编写一个,但那样您就 必须 确保对象不会被改变。

    这一点是为什么可变结构如列表、字典或集合不是可哈希的,而不可变结构如元组或 frozenset 是的原因:
    第 1 点和第 2 点要求哈希随内容变化,而第 3 点则禁止其变化。

有关此主题的更详细解释,请参阅此博客文章:Python 哈希与相等性

备注

请注意,unsafe_hash 参数的原始名称是 hash,但在 22.2.0 中已更改以符合 PEP 681
旧参数名称仍然存在,并且 不会 被删除——但设置 unsafe_hash 优先于 hash
字段级参数仍称为 hash,并将保持不变。

哈希与可变性(Hashing and Mutability)

在第一次调用 __hash__ 之后更改任何参与哈希代码计算的字段(通常这将在其插入哈希基础集合之后)可能会导致静默错误。
因此,强烈建议哈希可用的类应为 frozen
但是,请注意,这并不能完全保证安全:
如果一个字段指向一个对象,而该对象被修改,则哈希代码可能会改变,但 frozen 并不会保护您。

哈希代码缓存(Hash Code Caching)

某些对象的哈希代码计算开销较大。
如果这些对象要存储在基于哈希的集合中,仅计算一次哈希代码并将结果存储在对象上以加速未来的哈希代码请求可能会很有用。
要启用哈希代码的缓存,请传递 @define(cache_hash=True)
这只能在 attrs 已经为对象生成哈希函数的情况下进行。