是啥让用户定义的类不可散列?

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。在任何情况下,您都不希望这种行为,因为使用ids 作为键并不是一个好主意。最好只转换为元组并对其进行哈希处理。实际上——我很抱歉;对我来说,这只是解决问题的一种相当复杂的方法。 在 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

【讨论】:

以上是关于是啥让用户定义的类不可散列?的主要内容,如果未能解决你的问题,请参考以下文章

是啥让线程的执行顺序不可预测?

使 python 用户定义的类可排序、可散列

是啥让某些东西成为 ASP.NET Core 中的请求功能?

是啥让不安全的脚本“不安全”?

允许用户在我的类中定义数组大小而不是使用固定变量的最佳方法是啥,在这种情况下为 8

是啥让某些 android 类“必须保留”?