《Hashable Objects Must Be Immutable》Posted by Al Sweigart in python - 中译版

哈希对象必须是不可变的

由 Al Sweigart 发布(In Python)

最近一篇给 Reddit 的文章引发了一些评论,所以我想澄清一下:在Python中,可哈希对象必须是不可变的,而且可变对象不能被哈希(只有一个例外)。

在收集火把和干草叉之前,让我解释一下背景。

如果你想要让你的类(class)可哈希,你必须遵守在 Python Glossary for the entry for ''hashable'' 的两个规则。

规则如下:

一个对象是可哈希的,如果:

【1】它的哈希值在其生存期内从不更改它需要一个 __hash__() 方法),而且与其他对象对比(它需要一个 __eq__() 方法)。

【2】比较等于的哈希对象必须具有相同的哈希值。

 

在Python中,整型,浮点数和布尔值都是不可变的。而且因为 1 == 1.0 == True ,然后 hash(1) == hash(1.0) == hash(True)

让我们创建一个不可变的Point类,只有可读的 x 和 y 属性,它将哈希用于元组:

>>> class ImmutablePoint:
...   def __init__(self, x, y):
...     self._x = x
...     self._y = y
...   def __eq__(self, other):
...     if not isinstance(other, ImmutablePoint):
...       return False
...     return self._x == other._x and self._y == other._y
...   def __hash__(self):
...     return hash((self._x, self._y))
...   @property
...   def x(self):
...     return self._x
...   @property
...   def y(self):
...     return self._y

我们可以创造 ImmutablePoint对象,它们是不可变的,所以我们不能改变它们的 x 或者 y 属性:

>>> ip = ImmutablePoint(10, 20)

>>> ip.x, ip.y
(10, 20)

>>> ip.x = 'changed' # x 和 y 是只读属性
Traceback (most recent call last):
  File "stdin", line 1, in module
AttributeError: can't set attribute

>>> ip2 = ImmutablePoint(10, 20)

>>> ip == ip2
True

因为它们都是可哈希的,所以我们也可以使用这些对象作为字典的键:

>>> d = {ip: 'hello'}

>>> d[ip]
'hello'

>>> d[ip2]
'hello'

到这里,因为哈希是基于对象的值和对象的哈希永远不会改变(根据来自 Python Glossary 的规则),这必然意味着只有不可变的对象可以哈希。注意到你不能使用可变的列表或者字典作为字典的键,而且也无法使用内建函数 hash() 返回任何东西。

>>> {[1, 2, 3]: 'hello'}
Traceback (most recent call last):
  File "stdin", line 1, in module
TypeError: unhashable type: 'list'

>>> hash([1, 2, 3])
Traceback (most recent call last):
  File "stdin", line 1, in module
TypeError: unhashable type: 'list'

>>> hash({1:2, 3:4})
Traceback (most recent call last):
  File "stdin", line 1, in module
TypeError: unhashable type: 'dict'

但是为什么是这样呢?使可变对象哈希有什么不好呢?我们可以在可变类中添加一个 hash() 方法,对么?

原因跟字典如何使用哈希有关。这将需要一些关于字典/哈希图(hashmap)如何工作的CS背景(但是你可以观看 Brandon Rhode 的 PyCon 2010 演讲,The Mighty Dictionary【强大的字典】)。但是简单的回答是,如果对象的值更改,则哈希值也必须更改,因为哈希值是基于值的。但是,如果对象的值在用作字典中的键后发生更改,则哈希将不再引用字典中该键的正确的表元(bucket)。让我们举一个例子说明。

这里有一个Python内建列表数据类型(可变的)的子类,我们给它加上了 __hash__() 函数:

>>> import collections

>>> class HashableList(collections.UserList):
...     def __hash__(self):
...         return hash(tuple(self))

我们创建一个可哈希的列表对象,并把它放到字典中去:

>>> h = HashableList([1, 2, 3])

>>> d = {h: 'hello'}

>>> d
{[1, 2, 3]: 'hello'}

这好像起作用了。我们甚至用相同的值创建另一个可哈希列表,这看上去也起作用了:

>>> d[h]
'hello'

>>> h2 = HashableList([1, 2, 3])

>>> d[h2]
'hello'

现在我们改变 h 。如预期的那样,它产生了 KeyError 因为 [1, 2, 100] 在不是字典的键,而只有 [1, 2, 3] 是字典的键(这被证明也错的,但是现在请忽略它):

>>> h[2] = 100

>>> d[h]
Traceback (most recent call last):
  File "stdin", line 1, in module
KeyError: [1, 2, 100]

但是问题是。h2 也不再不是字典的键,即时它是 [1, 2, 3]

>>> d[h2]
Traceback (most recent call last):
  File "stdin", line 1, in module
KeyError: [1, 2, 3]

这是为什么呢?因为 d 字典中键不是 [1, 2, 3] 对象的副本,它是引用的副本。当我们改变 h,我们也改变了字典的键:

>>> d
{[1, 2, 100]: 'hello'}

所以这意味着键现在是 [1, 2, 100] ,但它在 [1,2,3]表元(bucket)/ slot(槽?)中。但是 h2[1, 2, 3] 不起作用,因为字典键的值现在是 [1, 2, 100],Python只是假设它恰好是哈希冲突。。

要使情况更糟,请尝试将 h[2] 设置为 99 。纯属巧合的是,[1,2,99] 哈希到与 [1,2,3] 相同的表元(bucket)/ slot(槽?)。并且这些值是相同的(因为键已经被改变到 [1, 2, 99])所以它工作得非常好,即使它应该导致 KeyError

>>> h[2] = 99 # 根据您的python版本,您可能需要找到另一个整数来产生它。

>>> d[h]
'hello'

所以这就是为什么可变对象被哈希是一件糟糕的事情:更改它们也会更改字典中的键。有时这会起作用即使它应该导致 KeyError(偶然发生哈希冲突时)而其他时间当它本起作用时,它会产生 KeyError(因为可变对象更改时字典键已更改)

但是等一下,仅有的一个例外是什么?

好吧,一个可变的对象可以被哈希,并且只要它的哈希值和值与它的身份(identity)(或者其他一些唯一不变的整数)相同,就可以用作字典键。那就是说,它必须有以下的 __eq__()__hash__() 实现:

>>> class SomeClass:
...   def __eq__(self, other):
...     return other is self
...   def __hash__(self):
...     return id(self)

这也恰好是Python类的默认实现,因此我们可以将类缩短为以下内容:

>>> class SomeClass:
...   pass

在这种情况下,我们支持哈希性的两个规则。但是我们没有一个真正有用的类,因为 Someclass 类型的每个对象都只等于它本身,并且与任何其他对象相比,无论它的值是什么,总是 False

>>> sc = SomeClass()

>>> {sc: 'hello'}
{<__main__.SomeClass object at 0x000001C6CEA57748>: 'hello'}

>>> sc2 = SomeClass()

>>> sc == sc2
False

因此,要么您可以为类遵循Python的两个哈希规则,要么您可以创建不在字典中实际工作的可变的哈希对象。唯一的例外是,当您有一个可变的哈希类时,哈希是基于身份(identity)而不是值的,这严重限制了它作为字典键的实用性。这意味着,实际上,只有不可变的对象才是可哈希的,可变的对象不能是可哈希的。

完。

转载网站:https://inventwithpython.com/blog/2019/02/01/hashable-objects-must-be-immutable/

作者:Al Sweigart

参考翻译:

《Python:说说字典和散列表,散列冲突的解决原理》

https://www.cnblogs.com/gl1573/archive/2018/10/09/9758941.html

 

最后,翻译可能不是很到位,可能有误,尽力了,但也学到了很多。

 

猜你喜欢

转载自blog.csdn.net/Enderman_xiaohei/article/details/87426025
AL