是否真的有必要对比较相同的类进行哈希处理?

时间:2016-07-27 20:03:03

标签: python class hash equivalence

似乎在阅读this answer时,如果在自定义类中定义__eq__,则还需要定义__hash__。这是可以理解的 但是,目前尚不清楚,为什么 - 有效 - __eq__应与self.__hash__()==other.__hash__

相同

想象一下这样的课:

class Foo:
    ...
    self.Name
    self.Value
    ...
    def __eq__(self,other):
        return self.Value==other.Value
    ...
    def __hash__(self):
        return id(self.Name)

这样,类实例可以通过值进行比较,这可能是唯一合理的用途,但在名称上被视为相同 这种方式set不能包含具有相同名称的多个实例,但比较仍然有效。

这种定义可能会出现什么问题?

__eq____lt__和其他Value定义的原因是能够按Value对实例进行排序,并且能够使用max等函数。例如,他的类应该代表设备的物理输出(比如加热元件)。每个输出都有唯一的名称。值是输出设备的功率。为了找到加热元件的最佳组合以便打开,能够通过功率(值)进行比较是有用的。但是,在集合或字典中,不应该有多个具有相同名称的输出。当然,具有不同名称的不同输出可能很容易具有相同的功率。

2 个答案:

答案 0 :(得分:6)

问题在于它没有意义,哈希用于对对象进行有效的分组。因此,当您有一个实现为哈希表的集合时,每个哈希都指向一个桶,它通常是一个元素列表。为了检查元素是否在集合(或其他基于散列的容器)中,您转到散列指向的存储桶,然后迭代列表中的所有元素,逐个进行比较。

换句话说 - 哈希不应该是比较器(因为它可以,并且应该给你有时误报)。特别是,在您的示例中,您的集合将无法工作 - 它将无法识别重复,因为它们不会相互比较。

class Foo:

    def __eq__(self,other):
        return self.Value==other.Value

    def __hash__(self):
        return id(self.Name)


a = set()
el = Foo()
el.Name = 'x'
el.Value = 1

el2 = Foo()
el2.Name = 'x'
el2.Value = 2

a.add(el)
a.add(el2)
print len(a) # should be 1, right? Well it is 2

实际上它更糟糕的是,如果你有两个具有相同值但名称不同的对象,它们不会被识别为相同

class Foo:

    def __eq__(self,other):
        return self.Value==other.Value

    def __hash__(self):
        return id(self.Name)


a = set()
el = Foo()
el.Name = 'x'
el.Value = 2

el2 = Foo()
el2.Name = 'a'
el2.Value = 2

a.add(el)
a.add(el2)
print len(a) # should be 1, right? Well it is 2 again

正确地做(因此,"如果a == b,那么哈希(a)==哈希(b)")给出:

class Foo:

    def __eq__(self,other):
        return self.Name==other.Name

    def __hash__(self):
        return id(self.Name)


a = set()
el = Foo()
el.Name = 'x'
el.Value = 1

el2 = Foo()
el2.Name = 'x'
el2.Value = 2

a.add(el)
a.add(el2)
print len(a) # is really 1

更新

还有一个非确定性部分,很难轻易复制,但实质上哈希并不能唯一地定义一个存储桶。通常就像

bucket_id = hash(object) % size_of_allocated_memory 
因此,具有不同散列的东西仍然可以在同一个桶中结束。因此,即使名称不同,您也可以获得两个等于每个元素(内部集合)的元素,即使名称不同,反之亦然,这取决于实际的内部实现,内存约束等。

一般来说,还有更多的例子会出现问题,因为哈希被定义作为函数h : X -> Z,因此x == y => h(x) == h(y),因此人们实现了他们的容器,授权协议和其他工具可以假设这个属性。如果你打破它 - 使用哈希的每一个工具都可能破坏。此外,它可以突破,这意味着您更新了一些库并且您的代码将停止工作,因为对底层库的有效更新(使用上述假设)可能会导致您违反此行为假设。

更新2

最后,为了解决您的问题 - 您不应该定义 eq lt 运算符来处理排序。这是关于元素的实际比较,它应该与其他行为兼容。您所要做的就是定义一个单独的比较器并在您的排序例程中使用它(在python中排序接受任何比较器,您不需要依赖<,>等)。另一种方法是在值上定义有效的&lt ;,>,=,但是为了使名称保持唯一 - 保留一个带有... well ...名称的集合,而不是对象本身。无论你选择哪条路 - 这里的关键因素是: 平等和散列必须兼容,这就是全部。

答案 1 :(得分:-1)

可以像这样实现你的类,有任何问题。但是,您必须100%确定没有两个不同的对象将生成相同的哈希。请考虑以下示例:

class Foo:
    def __init__(self, name, value):
        self.name= name
        self.value= value

    def __eq__(self, other):
        return self.value == other.value

    def __hash__(self):
        return hash(self.name[0])

s= set()
s.add(Foo('a', 1))
s.add(Foo('b', 1))
print(len(s)) # output: 2

但是如果发生哈希冲突,你会遇到问题:

s.add(Foo('abc', 1))
print(len(s)) # output: 2

为了防止出现这种情况,您必须知道完全如何生成哈希值(如果您依赖idhash等函数,则可能会有所不同实现之间!)以及用于生成散列的属性的值(在此示例中为name)。这就是为什么排除哈希冲突的可能性是非常困难的,如果不是不可能的话。这基本上就像是在乞求意想不到的事情发生。