是啥让用户定义的类不可散列?
Posted
技术标签:
【中文标题】是啥让用户定义的类不可散列?【英文标题】:What makes a user-defined class unhashable?是什么让用户定义的类不可散列? 【发布时间】:2012-05-02 12:47:17 【问题描述】:docs 表示只要定义了__hash__
方法和__eq__
方法的类是可散列的。然而:
class X(list):
# read-only interface of `tuple` and `list` should be the same, so reuse tuple.__hash__
__hash__ = tuple.__hash__
x1 = X()
s = x1 # TypeError: unhashable type: 'X'
是什么让X
不可散列?
请注意,我必须有相同的列表(就常规相等而言)才能被散列到相同的值;否则,我将 violate this requirement 讨论哈希函数:
唯一需要的属性是比较相等的对象具有 相同的哈希值
文档确实警告说,在其生命周期内不应修改可散列对象,当然我不会在创建后修改 X
的实例。当然,解释器无论如何也不会检查。
【问题讨论】:
是的,只读接口是一样的,但是为什么你期望 tuple.__hash__ 只使用它自己类的外部接口呢?尤其是用 C 语言编写时。使用外部接口会慢得多。除非 B 类是 A 类的子类,否则您不能合理地期望 A 类中的方法适用于 B 类。您是否也尝试调用 x1.__hash__() 来查看它是否有效? @LennartRegebro 是的,我同意...请参阅我对***.com/a/10254636/336527的最后评论...我只是大脑冻结。 【参考方案1】:仅将__hash__
方法设置为tuple
类的方法是不够的。你实际上并没有告诉它如何以不同的方式散列。元组是可散列的,因为它们是不可变的。如果你真的想让你的具体示例工作,它可能是这样的:
class X2(list):
def __hash__(self):
return hash(tuple(self))
在这种情况下,您实际上是在定义如何散列您的自定义列表子类。您只需要准确定义它如何生成哈希即可。您可以随意散列,而不是使用元组的散列方法:
def __hash__(self):
return hash("foobar"*len(self))
【讨论】:
但是tuple.__hash__
不是一个接受一个元组并返回一个数字的函数吗?该函数如何“注意到”我的对象实际上是 list
而不是 tuple
- 这两种类型的读取 API 是相同的。
@max: tuple.__hash__
是元组类的绑定方法。您不会更改其实现在该方法中所做的任何事情以进行散列。定义你自己的。
hash((1,2,3))
与(1,2,3).__hash__
相同;这和tuple.__hash__((1,2,3))
一样,对吧?所以tuple.__hash__
依赖于类tuple
的非公共API,因此当传递与tuple
的公共API 匹配的不同类的实例时,它会出现令人困惑的错误消息?我想它解释了它..但有点出乎意料。`
@max:最终哈希过程是在元组类__hash__
中定义的,不看源代码我只能假设它专门用于元组实例的内部。简单地将其方法引用传递给您的列表类并没有按预期工作,我一点也不感到惊讶。
@max 方法通常依赖于类的内部结构。你真的期望能够在类 A 上实现一个方法并将其应用到类 B 的对象上,仅仅因为两个类的公共 API 有一些相似之处吗? tuple 和 list 是用 C 实现的内置类,这一事实使得它更不太可能起作用;在 Python 级别,如果 B 对象具有您从 A 获得的方法所需的所有属性,那么这可以工作,但在 C 级别,我们谈论的是结构、数组和指针。【参考方案2】:
如果您在创建后不修改 X
的实例,为什么不子类化元组?
但我要指出,这实际上不会引发错误,至少在 Python 2.6 中是这样。
>>> class X(list):
... __hash__ = tuple.__hash__
... __eq__ = tuple.__eq__
...
>>> x = X()
>>> s = set((x,))
>>> s
set([[]])
我不敢说“有效”,因为这并没有达到你认为的效果。
>>> a = X()
>>> b = X((5,))
>>> hash(a)
4299954584
>>> hash(b)
4299954672
>>> id(a)
4299954584
>>> id(b)
4299954672
它只是使用对象 id 作为哈希。当您实际调用__hash__
时,您仍然会收到错误消息;同样适用于__eq__
。
>>> a.__hash__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: descriptor '__hash__' for 'tuple' objects doesn't apply to 'X' object
>>> X().__eq__(X())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: descriptor '__eq__' for 'tuple' objects doesn't apply to 'X' object
我收集到 python 内部,出于某种原因,正在检测 X
有一个 __hash__
和一个 __eq__
方法,但没有调用它们。
这一切的寓意是:只需编写一个真正的哈希函数。由于这是一个序列对象,因此将其转换为元组和散列是最明显的方法。
def __hash__(self):
return hash(tuple(self))
【讨论】:
很抱歉,这个问题脱离了另一个问题的上下文。我只是对这种特殊行为感到困惑。我子类列表的原因有点复杂(参见 cmets 到 this question 的讨论)。 该代码在 ActiveState Python 3.2 中对我不起作用。也许最近行为发生了变化? 我使用的是 Python 2.6。在任何情况下,您都不希望这种行为,因为使用id
s 作为键并不是一个好主意。最好只转换为元组并对其进行哈希处理。实际上——我很抱歉;对我来说,这只是解决问题的一种相当复杂的方法。
在 Python 3 中,如果我正确理解代码,元组哈希确实会生成元组对象的哈希,而不仅仅是元组 ID。
@LennartRegebro,我认为在 Python 2 中也必须如此;或者至少我能够创建两个具有不同 id 的元组,它们评估为相等并具有相同的哈希值。我在这里描述的行为仅适用于上面定义的X
对象。【参考方案3】:
根据您的其他问题,您可以而且应该做的是: 不要子类化任何东西,只需封装一个元组。在 init 中这样做完全没问题。
class X(object):
def __init__(self, *args):
self.tpl = args
def __hash__(self):
return hash(self.tpl)
def __eq__(self, other):
return self.tpl == other
def __repr__(self):
return repr(self.tpl)
x1 = X()
s = x1
产生:
>>> s
set([()])
>>> x1
()
【讨论】:
你是对的,对于许多用例来说,这是最干净、最简单的解决方案; +1【参考方案4】:来自 Python3 文档:
如果一个类没有定义一个 __eq__() 方法,它就不应该定义一个 __hash__() 操作;如果它定义了 __eq__() 而不是 __hash__(),它的实例将不能用作可散列集合中的项目。如果一个类定义了可变对象并实现了一个 __eq__() 方法,它不应该实现 __hash__(),因为哈希集合的实现需要一个键的哈希 value 是不可变的(如果对象的哈希值发生变化,它将在 错误的哈希桶)。
参考:object.__hash__(self)
示例代码:
class Hashable:
pass
class Unhashable:
def __eq__(self, other):
return (self == other)
class HashableAgain:
def __eq__(self, other):
return (self == other)
def __hash__(self):
return id(self)
def main():
# OK
print(hash(Hashable()))
# Throws: TypeError("unhashable type: 'X'",)
print(hash(Unhashable()))
# OK
print(hash(HashableAgain()))
【讨论】:
__hash__
是否需要唯一?假设您希望根据您在__eq__
中定义的标准比较HashableAgain
的实例,您可以在__hash__
中返回一个常量整数吗? (我真的不明白如何使用哈希)来决定一个对象在集合中的成员资格。
@MinhTran:一般来说,散列不是唯一的,而是相对唯一的。它用于在地图中存储值。如果你为哈希使用一个常量值,所有的值都会出现在同一个桶中,所以性能会很糟糕......但它应该仍然可以工作!【参考方案5】:
对上述答案的补充 - 对于 python3.7+ 中数据类的特定情况 - 要使数据类可散列,您可以使用
@dataclass(frozen=True)
class YourClass:
pass
作为装饰而不是
@dataclass
class YourClass:
pass
【讨论】:
以上是关于是啥让用户定义的类不可散列?的主要内容,如果未能解决你的问题,请参考以下文章
是啥让某些东西成为 ASP.NET Core 中的请求功能?