如何在不使它们不可变的情况下使 python 数据类可散列?
Posted
技术标签:
【中文标题】如何在不使它们不可变的情况下使 python 数据类可散列?【英文标题】:How can I make a python dataclass hashable without making them immutable? 【发布时间】:2019-02-22 17:25:45 【问题描述】:假设我在 python3 中有一个数据类。我希望能够对这些对象进行散列和排序。我不希望这些是不可变的。
我只希望它们在 id 上排序/散列。
我在文档中看到我可以实现 _hash_ 以及所有这些,但我想让 datacalsses 为我完成这项工作,因为它们旨在处理这个问题。 p>
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'
【问题讨论】:
要查找示例,请参阅本文***.com/a/52283085/4531270 中的您可以打开什么部分。 设置@dataclass(frozen=True)
@MartinThoma Frozen 将导致数据类模拟只读实例。这不是我想要的。
@BrianC。 Frozen 还使数据类可散列。
@MartinThoma 我不想要不可变对象,因此冻结对我的情况不起作用。我已经编辑了问题以添加它。
【参考方案1】:
来自the docs:
以下是管理
__hash__()
方法的隐式创建的规则:[...]
如果
eq
和frozen
都为真,默认情况下dataclass()
将 为您生成一个__hash__()
方法。如果eq
为真且frozen
为假,__hash__()
将设置为None
,将其标记为不可散列 (确实如此,因为它是可变的)。如果eq
为假,__hash__()
将保持不变,这意味着__hash__()
的方法 将使用超类(如果超类是对象,这意味着它 将回退到基于 id 的哈希)。
由于您设置了 eq=True
并将 frozen
保留为默认值 (False
),因此您的数据类是不可散列的。
您有 3 个选项:
设置frozen=True
(除了eq=True
),这将使您的类不可变和可散列。
设置unsafe_hash=True
,这将创建一个__hash__
方法但使您的类保持可变,因此如果您的类的实例在存储在字典或集合中时被修改,则可能会出现问题:
cat = Category('foo', 'bar')
categories = cat
cat.id = 'baz'
print(cat in categories) # False
手动实现__hash__
方法。
【讨论】:
如下所述,要排除某些字段用于 unsafe_hash 中的 hash 生成,您可以使用 field(compare=False) 或 field(hash=False) (hash如果未设置,则继承比较值。)。 请注意,在 ID 类型字段的情况下手动实现__hash__()
是微不足道的:def __hash__(self): return hash(self.id)
【参考方案2】:
TL;DR
将frozen=True
与eq=True
结合使用(这将使实例不可变)。
长答案
来自docs:
__hash__()
被内置的hash()
使用,并且当对象被添加到哈希集合(如字典和集合)时。有一个__hash__()
意味着类的实例是不可变的。可变性是 复杂的属性取决于程序员的意图,__eq__()
的存在和行为,以及 eq 和dataclass()
装饰器中的冻结标志。默认情况下,
dataclass()
不会隐式添加__hash__()
方法 除非这样做是安全的。它也不会添加或更改现有的 明确定义__hash__()
方法。设置类属性__hash__ = None
对 Python 有特定的含义,如__hash__()
documentation 中所述。如果
__hash__()
没有明确定义,或者如果它设置为无,那么dataclass()
可以添加一个隐式的__hash__()
方法。虽然不是 推荐,可以强制dataclass()
创建__hash__()
方法 与unsafe_hash=True
。如果您的班级是 逻辑上不可变,但仍然可以变异。这是一个 专门的用例,应该仔细考虑。以下是管理
__hash__()
方法的隐式创建的规则。 请注意,您不能同时在您的 数据类并设置unsafe_hash=True
;这将导致TypeError
。如果 eq 和 freeze 都为真,默认情况下
dataclass()
会生成一个__hash__()
适合您的方法。如果 eq 为真而frozen 为假,__hash__()
将设置为 None,将其标记为不可散列(它是,因为它是可变的)。如果 eq 为假,__hash__()
将被留下 untouched 意味着将使用超类的__hash__()
方法 (如果超类是对象,这意味着它将回退到基于 id 哈希)。
【讨论】:
【参考方案3】:我想为 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
【讨论】:
如果您将节点存储在图表中但希望在不破坏其散列的情况下标记它们已访问(例如,如果它们位于一组未访问的节点中......),这可能很有用。 i>:我从来没有觉得一个示例用例更有针对性。以上是关于如何在不使它们不可变的情况下使 python 数据类可散列?的主要内容,如果未能解决你的问题,请参考以下文章