87

假设我在 python3 中有一个数据类。我希望能够对这些对象进行散列和排序。我不希望这些是不可变的。

我只希望他们在 id 上排序/散列。

我在文档中看到我可以实现 _ hash _ 以及所有这些,但我想让 datacalsses 为我完成这项工作,因为它们旨在处理这个问题。

from dataclasses import dataclass, field

@dataclass(eq=True, order=True)
class Category:
    id: str = field(compare=True)
    name: str = field(default="set this in post_init", compare=False)

a = sorted(list(set([ Category(id='x'), Category(id='y')])))

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'Category'
4

3 回答 3

88

文档

以下是管理方法隐式创建的规则__hash__()

[...]

如果eqfrozen都为真,默认情况下dataclass()会为你生成一个__hash__()方法。如果eq为真且frozen 为假,__hash__()将设置为None,将其标记为不可散列(它是,因为它是可变的)。如果eq为假,__hash__() 将保持不变__hash__(),这意味着将使用超类的方法(如果超类是对象,这意味着它将回退到基于 id 的散列)。

由于您设置eq=True并保留frozen默认值 ( False),因此您的数据类是不可散列的。

您有 3 个选项:

  • Set frozen=True(除了eq=True),这将使您的类不可变和可散列。
  • Set unsafe_hash=True,它将创建一个__hash__方法但使您的类保持可变,因此如果您的类的实例在存储在 dict 或 set 中时被修改,则可能会出现问题:

    cat = Category('foo', 'bar')
    categories = {cat}
    cat.id = 'baz'
    
    print(cat in categories)  # False
    
  • 手动实现一个__hash__方法。
于 2018-09-18T16:14:45.893 回答
25

TL;博士

frozen=True与 to 结合使用eq=True(这将使实例不可变)。

长答案

文档

__hash__()由 built-in 使用hash(),并且在将对象添加到散列集合(例如字典和集合)时使用。拥有 a__hash__() 意味着类的实例是不可变的。可变性是一个复杂的属性,它取决于程序员的意图、 的存在和行为,以及装饰器__eq__()中 eq 和 freeze 标志的值。dataclass()

默认情况下,除非这样做是安全的,否则dataclass()不会隐式添加方法。__hash__()它也不会添加或更改现有的明确定义的__hash__()方法。如文档中所述,设置类属性 __hash__ = None对 Python 具有特定含义。__hash__()

如果__hash__()没有显式定义,或者如果它设置为 None,那么 dataclass()可以添加一个隐式__hash__()方法。虽然不推荐,但您可以强制dataclass()使用__hash__(). unsafe_hash=True如果您的类在逻辑上是不可变的,但仍然可以发生变异,则可能会出现这种情况。这是一个专门的用例,应该仔细考虑。

以下是管理方法隐式创建的规则__hash__()。请注意,您不能__hash__()在数据类和 set 中都有显式方法unsafe_hash=True;这将导致一个TypeError.

如果 eq 和 freeze 都是 true,默认情况下dataclass()会为你生成一个 __hash__()方法。如果 eq 为 true 并且 freeze 为 false,__hash__()将设置为 None,将其标记为不可散列(它是,因为它是可变的)。如果 eq 为假,__hash__()将保持不变__hash__(),这意味着将使用超类的方法(如果超类是对象,这意味着它将回退到基于 id 的散列)。

于 2018-09-18T16:14:33.853 回答
24

我想为 unsafe_hash 的使用添加一个特别说明。

您可以通过设置 compare=False 或 hash=False 将字段排除在哈希比较之外。(哈希默认继承自比较)。

如果您将节点存储在图表中但希望在不破坏其散列的情况下将它们标记为已访问(例如,如果它们位于一组未访问的节点中......),这可能很有用。

from dataclasses import dataclass, field
@dataclass(unsafe_hash=True)
class node:
    x:int
    visit_count: int = field(default=10, compare=False)  # hash inherits compare setting. So valid.
    # visit_count: int = field(default=False, hash=False)   # also valid. Arguably easier to read, but can break some compare code.
    # visit_count: int = False   # if mutated, hashing breaks. (3* printed)

s = set()
n = node(1)
s.add(n)
if n in s: print("1* n in s")
n.visit_count = 11
if n in s:
    print("2* n still in s")
else:
    print("3* n is lost to the void because hashing broke.")

这花了我几个小时才弄清楚……我发现的有用的进一步阅读材料是关于数据类的 python 文档。具体参见字段文档和数据类 arg 文档。 https://docs.python.org/3/library/dataclasses.html

于 2019-07-29T03:16:26.673 回答