为什么可变的内置对象无法在Python中进行哈希处理?这有什么好处?

时间:2019-05-06 21:54:07

标签: python python-3.x

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

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

  

可哈希

     

如果对象的哈希值在其生命周期内始终不变(需要使用__hash__()方法,并且可以与其他对象进行比较(需要使用__eq__()方法),则该对象是可哈希的。比较相等的可哈希对象必须具有相同的哈希值。

     

可散列性使对象可用作字典键和set成员,因为这些数据结构在内部使用散列值。

     

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

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

  

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

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

3 个答案:

答案 0 :(得分:2)

在Python中,可变对象 可以是可散列的,但这通常不是一个好主意,因为通常来讲, quality 是根据这些可变属性定义的,这会导致各种疯狂的行为。

如果内置的可变对象是根据身份进行哈希处理的,例如用户定义对象的默认哈希处理机制,则它们的哈希将与它们的相等性不一致。这绝对是一个问题。但是,默认情况下,用户定义的对象会根据身份进行比较和散列,因此,虽然这组事务不是很有用,但情况并非如此。

请注意,如果您在用户定义的类中实现__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)时。

答案 1 :(得分:1)

计算哈希值就像为对象赋予一个标识,从而简化了对象的比较。按哈希值进行比较通常比按值进行比较要快:对于一个对象,您可以比较其属性,对于一个集合,则可以递归地比较其项...

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

编辑:Java HashCode

通常,hashCode()仅在不覆盖对象的情况下返回内存中的对象地址。

有关hashCode函数,请参见reference

  

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

因此,Java hashCode函数的功能与默认的Python __hash__函数相同。

例如,在Java中,如果您在HashSet中使用了可变对象,则HashSet不能正常工作。由于hashCode取决于对象的状态,因此无法再正确检索它,因此对包含的检查失败。

答案 2 :(得分:1)

从阅读其他评论/答案来看,您似乎不愿意购买的是,在突变时,您必须更改可变实体的哈希,并且您只能用id进行哈希,所以我'将尽力阐述这一点。

引用您的话

  

@kindall Hm ...谁说哈希值必须来自列表中的值?那如果你例如添加一个新值,您必须重新哈希该列表,获取一个新的哈希值,等等。在其他语言中,事实并非如此……这是我的观点。在其他语言中,哈希值仅来自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!