【问题标题】:Is it really necessary to hash the same for classes that compare the same?对于比较相同的类,是否真的需要散列相同的哈希值?
【发布时间】:2016-07-28 04:46:54
【问题描述】:

阅读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 不能包含多个具有相同名称的实例,但比较仍然有效。

这样的定义可能有什么问题?

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

【问题讨论】:

  • 如果Value 对平等很重要,您通常会将__hash__ 实现为return hash(self.Value)
  • 在这种情况下,值不足以表示身份,仅表示大小。通过值定义它 _eq 以及 lt 和其他是允许类按值排序的方法,但具有相同值的实例不被认为是相同的。
  • 你能举一个不那么抽象的例子吗?目前的问题似乎是你的类没有一致地定义。
  • 我用具体的例子更新了这个问题,希望它更清楚。
  • 在你的情况下,我建议平等将纯粹是在价值和散列上的名称和价值(return hash(self.Name) ^ hash(self.Value)“相等的对象需要散列相同” 是稍微简化一下,相同的对象应该散列相同。

标签: python class hash equivalence


【解决方案1】:

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

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

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

实际上更糟糕的是,如果您有 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,则 hash(a) == hash(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

更新

还有一个不确定的部分,很难轻易复现,但本质上 hash 并没有唯一定义一个桶。通常是这样的

bucket_id = hash(object) % size_of_allocated_memory 

因此,具有不同哈希值的事物最终仍可能在同一个存储桶中。因此,即使名称不同,您也可以获得两个相等的元素(内部集合),即使名称不同,也可以反过来,具体取决于实际的内部实现、内存限制等。

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

更新 2

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

【讨论】:

  • 你描述的第一个行为正是我想要的。
  • 当它爆炸时展示一个例子有点棘手,但你实际上可以有不同的名字,它们将放在同一个桶中,然后你会得到一个误报,因为值相等。 “id”应该是唯一的这一事实并不意味着实际的存储桶将是唯一的。换句话说 - 第一个例子是非确定性的。它有时会给你平等的元素,有时不会。取决于python解释器的内部实现、可用内存等。
  • @Lukas 现在你在自相矛盾。在问题中,您写了 “但是,在集合或字典中,不可能有多个具有相同名称的输出”
  • @Rawing:我错了,这是我想要的第二个,第一个应该是一个。
  • 而且两者都不是 1(也不能是),如示例中所示。 “第二个”行为需要将散列修复为原始定义(这是我的最后一个示例)
【解决方案2】:

这样实现你的类是可能的,没有有任何问题。但是,您必须 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)。这就是为什么排除哈希冲突的可能性非常困难,如果不是不可能的话。这基本上就像乞求意想不到的事情发生。

【讨论】:

  • 这根本不是真的。哈希不必唯一标识任何存储桶,哈希表(和其他容器)可以(并且确实)对您的哈希进行模运算(或其他映射到恒定大小的容器),因此即使是两个唯一的哈希也可能最终被存储在同一个地方,虽然这可以在容器级别解决(您可以在迭代时比较哈希),但这样做没有意义,因为“常规”相等应该起作用
  • @lejlot 这意味着避免哈希冲突比我说的更难。但这并不意味着我说的是错误的。
  • 这不是哈希冲突。这是两个不同的东西。正如您的回答一样,哈希冲突是当您有 x!=y 使得 h(x)==h(y) 时。我所描述的是您在实践中使用哈希时可能会遇到的一种错误。这些错误来自这样一个事实,即(如前面的回答中所说)有一个关于散列的潜在假设,即如果两个对象相等,它们的散列也相等。时期。这不是关于碰撞,而是关于哈希之后可能(并且将在某些时候)被破坏的常见逻辑。
猜你喜欢
  • 1970-01-01
  • 2020-03-26
  • 1970-01-01
  • 2021-11-15
  • 1970-01-01
  • 1970-01-01
  • 2014-09-22
  • 2021-12-26
  • 2020-05-13
相关资源
最近更新 更多