什么会导致用户定义的类无法哈希化?

68

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不可哈希?

请注意,我必须拥有相同的列表(在常规相等方面)才能哈希到相同的值;否则,我将违反哈希函数的要求

唯一需要的属性是比较相等的对象具有相同的哈希值

文档确实警告说,哈希对象在其生命周期内不应进行修改,当然,我不会在创建后修改X的实例。当然,解释器也不会检查那个。


2
是的,只读接口是相同的,但是为什么你期望tuple.__hash__仅使用其自身类的外部接口呢?特别是当它是用C写的时候。使用外部接口会慢得多。除非类B是从类A继承的,否则您不能合理地期望类A的方法适用于类B。您是否尝试调用x1.__hash __()以查看它是否起作用了呢? - Lennart Regebro
@LennartRegebro 是的,我同意...请看我的最后一条评论:https://dev59.com/kWkv5IYBdhLWcg3w71H7#10254636... 我只是脑子一片空白。 - max
5个回答

51

仅将__hash__方法设置为tuple类的方法是不够的。您实际上没有告诉它如何以不同的方式进行哈希。元组是可哈希的,因为它们是不可变的。如果您真的想让您的特定示例起作用,可能会像这样:

class X2(list):
    def __hash__(self):
        return hash(tuple(self))
在这种情况下,您实际上正在定义如何哈希自己的自定义列表子类。您只需明确定义它如何生成哈希即可。您可以按照所需进行哈希,而不必使用元组的哈希方法:
def __hash__(self):
    return hash("foobar"*len(self))

但是tuple.__hash__不是一个函数,它需要一个元组并返回一个数字吗?这个函数如何“注意到”我的对象实际上是一个列表而不是元组,因为这两种类型的读取API是相同的。 - max
@max:tuple.__hash__是元组类的绑定方法。你不能改变该方法内部实现的哈希操作。请自行定义新的哈希方法。 - jdi
hash((1,2,3))(1,2,3).__hash__ 是一样的;也就是说 tuple.__hash__((1,2,3)),对吧?所以 tuple.__hash__ 依赖于类 tuple 的非公共 API,因此当传递一个与 tuple 的公共 API 匹配的不同类的实例时,它会崩溃并显示令人困惑的错误消息。我想这解释了问题的原因,但有点出乎意料。 - max
2
@max:归根结底,哈希过程是在元组类的__hash__中定义的,而且在没有查看源代码的情况下,我只能假设它专门用于元组实例的内部。我并不惊讶,仅仅将其方法引用传递到您的列表类中并没有按预期工作。 - jdi
通常方法依赖于类的内部。你真的希望能够将类A上的方法的实现应用到类B的对象上,仅仅因为这两个类的公共API有些相似吗?元组和列表是在C中实现的内置类,这使得这种情况更加不可能发生;在Python级别上,如果B对象具有从A获取的方法所需的所有属性,则可能会起作用,但在C级别上,我们正在谈论结构体、数组和指针。 - Ben
@Ben 我同意...我不知道当时在想什么。 - max

29

来自Python3文档:

如果一个类没有定义__eq__()方法,则也不应该定义__hash__()操作;如果它定义了__eq__()但没有定义__hash__(),则它的实例将无法在可散列的集合中使用。如果一个类定义了可变对象并实现了__eq__()方法,则不应该实现__hash__(),因为可散列集合的实现要求键的哈希值是不可变的(如果对象的哈希值发生更改,它将位于错误的哈希桶中)。

Ref: 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()))

1
__hash__ 需要是独一无二的吗?假设你希望 HashableAgain 的实例根据你在 __eq__ 中定义的标准进行比较,那么在 __hash__ 中返回一个恒定的整数可以吗?(我不太理解哈希在集合成员的判断中是如何使用的。) - Minh Tran
1
@MinhTran:一般来说,哈希不是唯一的,但是相对唯一。它用于在映射中存储值。如果您为哈希使用常量值,则所有值都将出现在同一个桶中,因此性能会非常差...但它仍然可以工作! - kevinarpe

9
根据你之前的问题,你可以并且应该这样做: 不要继承任何东西,只需封装一个元组。在初始化时这样做是完全可以的。
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 - senderle

7

针对Python3.7+中dataclass的特定情况,若想使其可哈希,可以使用以下代码:

@dataclass(frozen=True)
class YourClass:
    pass

作为装饰而非
@dataclass
class YourClass:
    pass

3
如果您创建后不修改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))

非常抱歉,这个问题是从另一个问题的上下文中脱离出来的。我只是对这种特定的行为感到困惑。我子类化列表的原因有点复杂(请参见此问题中的评论讨论)。 - max
这段代码在我的ActiveState Python 3.2中无法运行。也许最近的行为有所改变了? - max
我正在使用Python 2.6。无论如何,您都不希望出现这种行为,因为使用“id”作为键并不是一个好主意。最好只是转换为元组并对其进行哈希。实际上 - 我很抱歉; 对于我来说,这只是一个相当令人困惑的问题解决方法。 - senderle
在Python 3中,如果我理解代码正确的话,元组哈希确实会对元组对象进行哈希处理,而不仅仅是元组ID。 - Lennart Regebro
@LennartRegebro,我认为在Python 2中也必须是这样的;或者至少我能够创建两个具有不同id但被求值为相等且具有相同哈希的元组。我在此描述的行为仅适用于上述定义的“X”对象。 - senderle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接