【问题标题】:Why mutable built-in objects cannot be hashable in Python? What is the benefit of this?为什么可变内置对象不能在 Python 中进行哈希处理?这有什么好处?
【发布时间】:2019-09-24 13:01:56
【问题描述】:

我来自 Java,即使是可变对象也可以是“可散列的”。
这些天我玩 Python 3.x 只是为了好玩。

这是 Python 中 hashable 的定义(来自 Python 词汇表)。

可散列

如果一个对象有一个在其生命周期内永远不会改变的哈希值(它需要一个__hash__() 方法),并且可以与其他对象进行比较(它需要一个__eq__() 方法),那么它就是可哈希的。比较相等的可散列对象必须具有相同的散列值。

Hashability 使对象可用作字典键和集合成员,因为这些数据结构在内部使用哈希值。

所有 Python 的不可变内置对象都是可散列的;可变容器(例如列表或字典)不是。默认情况下,作为用户定义类实例的对象是可散列的。它们都比较不相等(除了它们自己),它们的哈希值来自它们的id()

我读了它,我在想...... 仍然......为什么他们在 Python 中甚至不使可变对象成为可哈希对象?例如。使用与用户定义对象相同的 default 散列机制,即如上面最后两句所述。

默认情况下,作为用户定义类实例的对象是可散列的。它们都比较不相等(除了它们自己),它们的哈希值来自它们的 id()。

这感觉有点奇怪......所以用户定义的可变对象是可散列的(通过此默认散列机制),但内置的可变对象不可散列。这不只是使事情复杂化了吗?我看不出它带来了什么好处,有人可以解释一下吗?

【问题讨论】:

  • 要散列一个列表,你必须首先散列其中的所有内容;因为它是可变的,所以对它的任何更改都应该更改哈希。要散列一个元组,散列它的id() 就足够了,因为它不能被修改。用户通常期望散列用户定义的类的行为更像一个不可变对象(即使它是可变的),所以它就是这样做的,当然你可以让它以任何你喜欢的方式运行。
  • @kindall:元组不是由id 散列的。
  • @kindall 嗯...谁说哈希值必须来自列表中的值?如果你例如添加一个新值,您必须重新散列列表,获取新的散列值等。在其他语言中,情况并非如此......这是我的观点。在其他语言中,哈希值仅来自 id(或者是 id 本身,就像用户定义的可变 Python 对象一样)......好吧......我只是觉得它让 Python 中的事情有点太复杂了(尤其是对于初学者......不适合我)。无论如何...感谢这里的所有答案,我会继续关注新的答案。
  • @peter.petrov:在 Java 中,您来自的语言,集合 hashCode基于容器内容。例如,here are the List docs。其他任何东西都会被破坏和/或无用,因为equals 是基于集合内容的。
  • “通过让你将基于可变状态散列的可变对象放入 HashMaps”哇...我假设在 Java 中他们不会基于可变状态对列表进行散列,而只是基于 id 或其他东西(就像 Python 对其可变的用户定义对象所做的那样)。这个假设是错误的......这真的很奇怪(我的意思是Java方式)......所以我在这里问了一个关于Python的问题......但也学习(或刷新)了一些关于Java的东西。这确实是 Java 中的奇怪行为。我也不喜欢。现在,Python 方式对我来说确实更有意义。

标签: python python-3.x


【解决方案1】:

从阅读其他 cmets/answers 看来,您似乎没有购买的是,当可变实体发生变异时您必须更改它的哈希值,并且您可以通过 id 进行哈希处理,所以我会尝试详细说明这一点。

引用你的话:

@kindall 嗯...谁说哈希值必须来自列表中的值?如果你例如添加一个新值,您必须重新散列列表,获取新的散列值等。在其他语言中,情况并非如此......这是我的观点。在其他语言中,哈希值仅来自 id(或者是 id 本身,就像用户定义的可变 Python 对象一样)......好吧......我只是觉得它让 Python 中的事情有点太复杂了(尤其是对于初学者...不适合我)。

这不是完全错误的(虽然我不知道你引用的是什么“其他”语言),你可以这样做,但有一些非常可怕的后果:

class HashableList(list):
    def __hash__(self):
        return id(self)

x = HashableList([1,2,3])
y = HashableList([1,2,3])

our_set = {x}

print("Is x in our_set? ", x in our_set)
print("Is y in our_set? ", y in our_set)
print("Are x and y equal? ", x == y)

这个(出乎意料的)输出:

Is x in our_set?  True
Is y in our_set?  False <-- potentially confusing
Are x and y equal? True

这意味着哈希与相等性不一致,这简直令人困惑。

您可能会反驳“好吧,然后按内容散列”,但我认为您已经明白,如果内容发生变化,那么您会得到其他不良行为(例如):

class HashableListByContents(list):
    def __hash__(self):
        return sum(hash(x) for x in self)

a = HashableListByContents([1,2,3])
b = HashableListByContents([1,2,3])

our_set = {a}

print('Is a in our_set? ', a in our_set)
print('Is b in our_set? ', b in our_set)
print('Are a and b equal? ', a == b)

这个输出:

Is a in our_set?  True
Is b in our_set?  True
Are a and b equal?  True

到目前为止一切顺利!但是……

a.append(2)
print('Is a still in our set? ', a in our_set)

这个输出:

Is a still in our set?  False <-- potentially confusing

我不是 Python 初学者,所以我不知道什么会或不会让 Python 初学者感到困惑,但无论哪种方式,这对我来说似乎都令人困惑(充其量)。我的两分钱是散列可变对象是不正确的。我的意思是我们有功能纯粹主义者声称可变对象是不正确的,句号! Python 不会阻止你做任何你所描述的事情,因为它永远不会强迫这样的范式,但无论你走哪条路,它都是在自找麻烦。

HTH!

【讨论】:

    【解决方案2】:

    计算哈希值就像给一个对象一个标识,简化了对象的比较。哈希值比较通常比值比较快:对于一个对象,你比较它的属性,对于一个集合,你比较它的项目,递归......

    如果一个对象是可变的,您需要在每次更改后再次计算其哈希值。如果此对象与另一个对象比较相等,则在更改后它变得不相等。因此,可变对象必须按值进行比较,而不是通过哈希。通过哈希值比较可变对象是不发送的。

    编辑:Java HashCode

    通常,如果您不覆盖它,hashCode() 只会返回对象在内存中的地址。

    请参阅reference 了解hashCode 函数。

    在合理可行的情况下,hashCode 方法定义为 类 Object 确实为不同的对象返回不同的整数。 (这 通常是通过转换的内部地址来实现 对象转换成整数,但这种实现技术不是 JavaTM 编程语言所要求的。)

    因此,Java hashCode 函数与默认 Python __hash__ 函数的工作方式相同。

    在 Java 中,例如,如果您在 HashSet 中使用可变对象,HashSet 将无法正常工作。因为hashCode 依赖于对象的状态,它不能再被正确检索,所以包含检查失败。

    【讨论】:

    • 嗯......这再次对这个问题采取片面(Pythonic)观点。引用:“如果一个对象是可变的,你需要在每次更改后重新计算它的哈希值”这句话一般不正确,在 Python 中是正确的。
    • @peter.petrov 好吧,是的,但你问的是 Python
    • @juanpa.arrivillaga 嗯……好吧……是的,不是的……再深入一点,我想你明白我的意思了。
    • “因此,Java hashCode 函数的工作方式与默认 Python hash 函数相同。”是的,对于用户定义的对象 - 是的。这就是我问题的重点。为什么他们不让内置的不可变对象和用户定义的不可变对象在散列中表现得一致呢?听起来有些不平衡,这种行为不对称。没关系...我知道...我必须问做出决定的人 :) 这是一个深入/基本的语言设计。我只是好奇社区会怎么说。无论如何,感谢您的观点。
    • @peter.petrov:在这方面,内置类和正确实现的用户定义类的行为相同。没有不对称性。
    【解决方案3】:

    在Python中,可变对象可以是hashable的,但是一般来说不是一个好主意,因为一般来说相等就是根据这些可变属性来定义的,这可能会导致各种疯狂的行为。

    如果内置的可变对象基于身份进行散列,就像用户定义对象的默认散列机制一样,那么它们的散列将与它们的相等性不一致。这绝对是个问题。但是,用户定义的对象默认情况下会根据身份进行比较和散列,因此情况并没有那么糟糕,尽管这组事务不是很有用。

    注意,如果您在用户定义的类中实现__eq__,则__hash__ 将设置为None,从而使该类不可散列

    So, from the Python 3 data model documentation:

    用户定义的类有__eq__()__hash__()方法 默认;与它们相比,所有对象都比较不平等(除了 他们自己)和x.__hash__() 返回一个适当的值,使得 x == y 暗示 x is yhash(x) == hash(y)

    覆盖__eq__() 且未定义__hash__() 的类将其__hash__() 隐式设置为None。当。。。的时候 __hash__() 类的方法是None,当程序尝试检索时,该类的实例将引发适当的 TypeError 它们的哈希值,也将被正确识别为不可哈希 检查isinstance(obj, collections.abc.Hashable)时。

    【讨论】:

    • "如果内置的可变对象是基于身份进行哈希处理的,就像用户定义对象的默认哈希机制一样,那么它们的哈希值将与它们的相等性不一致。这绝对是个问题。"嗯...这是一个很好的观点...这是你说的一个问题...但是从计算机科学的角度来看...这是一个问题,因为平等是如何实现的。
    • 例如对我来说(因为我来自 Java) lst1 == lst2 返回 true (对于具有相同值的 2 个不同列表)似乎也很奇怪。所以......好吧......我想这只是一个非常基本的语言设计决定来质疑它(就像我在这里所做的那样)但仍然......似乎它让事情变得有点太复杂了。
    • @peter.petrov 在 Python 中,== 相当于 Java 的 .equals,而 Java 中的 == 相当于 Python 中的 is
    • 是的……我已经知道了……好的,好的,谢谢,我会再考虑一下。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-08
    • 2021-06-18
    • 2017-05-19
    • 1970-01-01
    相关资源
    最近更新 更多