从最近的一个SO问题(请参阅在python中创建由列表索引的字典)中,我意识到我可能对python中可哈希和不可变对象的含义有错误的理解。
- 在实践中,hashable是什么意思?
- 可哈希和不可修改之间有什么关系?
- 是否存在可哈希的可变对象或不可哈希的不可变对象?
从最近的一个SO问题(请参阅在python中创建由列表索引的字典)中,我意识到我可能对python中可哈希和不可变对象的含义有错误的理解。
Answers:
object
是可哈希的,但不是不可变的。这些实例可以使用具有字典键的键,但是如果传递它们,仍然可以对其进行修改。
- 是否存在可哈希的可变对象或不可哈希的不可变对象?
在Python中,元组是不可变的,但仅当其所有元素都是可哈希的时才可哈希。
>>> tt = (1, 2, (30, 40))
>>> hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40])
>>> hash(tl)
TypeError: unhashable type: 'list'
哈希类型
如果对象的哈希值在其生命周期内始终不变(需要一种
__hash__()
方法),并且可以与其他对象进行比较(需要一个__eq__()
或,则该对象是可哈希的)__cmp__()
方法),。比较相等的可哈希对象必须具有相同的哈希值。散列性使对象可用作字典键和set成员,因为这些数据结构在内部使用散列值。
Python的所有不可变内置对象都是可哈希的,而没有可变容器(例如列表或字典)是可哈希的。作为用户定义类实例的对象默认情况下可哈希化;它们都比较不相等,并且其哈希值是其id()。
字典和集合必须使用散列以在散列表中进行有效查找;散列值必须是不可变的,因为更改散列会破坏数据结构并导致dict或set失败。使哈希值不可变的最简单方法是使整个对象不可变,这就是为什么经常将两者一起提及的原因。
尽管内置的可变对象都不是可哈希的,但是可以使哈希对象的哈希值不可变。通常仅对象的一部分代表其身份,而对象的其余部分包含可以自由更改的属性。只要哈希值和比较函数基于身份而不是可变属性,并且身份永不更改,就可以满足要求。
id
)。这在对象的生存期内不会更改,因此可以进行哈希处理,但这并不意味着您无法定义可变类型!抱歉,但哈希性并不意味着不变。
从技术上讲,可哈希表示该类定义了__hash__()
。根据文档:
__hash__()
应该返回一个整数。唯一需要的属性是比较相等的对象具有相同的哈希值;建议以某种方式将散列值混合在一起(例如,使用异或),以将对象的组成部分也用作对象比较的一部分。
我认为对于Python内置类型,所有可哈希类型也是不可变的。
尽管如此,要定义一个可变的对象将是困难的,但并非并非不可能__hash__()
。
__hash__
是,默认情况下定义为返回对象的id
;。您必须全力以赴__hash__ = None
使其无法使用。同样,正如马克·兰瑟姆(Mark Ransom)所提到的那样,还有一个额外的条件,即只有在哈希值永远不会改变的情况下才可以哈希!
list
定义__hash__
在这个意义上hasattr([1,2,3], "__hash__")
的回报True
,但是调用hash([1,2,3])
加薪一TypeError
(Python 3中),所以它不是完全可哈希。依靠的存在__hash__
还不足以确定某物是否为a)可哈希化b)不可变
即使不可变和可散列之间没有强制的显式关系,也存在隐式关系
除非您重新定义,否则这里没有问题 __eq__
,因此objects类定义了值的等价关系。
完成之后,您需要找到一个稳定的哈希函数,该函数始终为表示相同值的对象返回相同的值(例如where __eq__
),返回True,并且在对象的生存期内始终不变。
很难找到可能的应用程序,请考虑满足这些要求的可能的A类。尽管存在明显的退化情况,其中__hash__
返回常数。
现在:-
>>> a = A(1)
>>> b = A(1)
>>> c = A(2)
>>> a == b
True
>>> a == c
False
>>> hash(a) == hash(b)
True
>>> a.set_value(c)
>>> a == c
True
>>> assert(hash(a) == hash(c)) # Because a == c => hash(a) == hash(c)
>>> assert(hash(a) == hash(b)) # Because hash(a) and hash(b) have compared equal
before and the result must stay static over the objects lifetime.
实际上,这意味着在创建时hash(b)== hash(c),尽管从来没有将其比较为相等。无论如何,我都很难看到__hash__
为定义按值比较的可变对象有用地定义()。
注:__lt__
,__le__
,__gt__
和__ge__
comparsions不受影响,所以你仍然可以定义哈希的对象,可变或以其他方式基于其数值的排序。
仅仅因为这是Google的热门话题,这是使可变对象可哈希化的一种简单方法:
>>> class HashableList(list):
... instancenumber = 0 # class variable
... def __init__(self, initial = []):
... super(HashableList, self).__init__(initial)
... self.hashvalue = HashableList.instancenumber
... HashableList.instancenumber += 1
... def __hash__(self):
... return self.hashvalue
...
>>> l = [1,2,3]
>>> m = HashableList(l)
>>> n = HashableList([1,2,3])
>>> m == n
True
>>> a={m:1, n:2}
>>> a[l] = 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> m.hashvalue, n.hashvalue
(0, 1)
实际上,当创建一个类将SQLAlchemy记录转换为对我来说更易变且更有用的东西,同时保持其哈希值以用作dict键时,我实际上发现了这种用法。
不变是指对象在其生存期内不会发生任何重大变化。在编程语言中,这是一个模糊但普遍的想法。
哈希性略有不同,指的是比较。
哈希的一个目的是可哈希如果它有一个哈希值其寿命(它需要一个在这期间从不改变
__hash__()
方法),并且可相对于其他对象(它需要一个__eq__()
或__cmp__()
方法)。比较相等的可哈希对象必须具有相同的哈希值。
所有用户定义的类都有 __hash__
方法,默认情况下,该方法仅返回对象ID。因此,满足散列性标准的对象不一定是不变的。
您声明的任何新类的对象都可以用作字典键,除非您通过例如从 __hash__
我们可以说所有不可变的对象都是可哈希的,因为如果哈希在对象的生命周期内发生变化,则意味着该对象已突变。
但不完全是。考虑一个具有列表(可变)的元组。有人说元组是不可变的,但同时它有些不可散列(抛出)。
d = dict()
d[ (0,0) ] = 1 #perfectly fine
d[ (0,[0]) ] = 1 #throws
哈希性和不变性是指对象实例,而不是类型。例如,一个元组类型的对象可以是不可哈希的。
comparison != identity
是将“无效”值(例如float("nan") == float("nan")
)或切片中的"apple" is "apple"
"apple" is "crabapple"[4:]
在Python中,它们几乎可以互换;因为哈希应该表示内容,所以它和对象一样易变,并且更改对象的哈希值会使它无法用作dict键。
在其他语言中,哈希值与对象“身份”更多相关,而与(不必)与该值相关。因此,对于可变对象,可以使用指针开始哈希。当然,假设某个对象不会在内存中移动(就像某些GC一样)。例如,这就是Lua中使用的方法。这使得可变对象可用作表键;但是会给新手带来一些(不愉快的)惊喜。
最后,拥有不可变的序列类型(元组)使其更适合“多值键”。
HashMap
如果您修改了用作键的对象,则a会损坏:即使打印地图也可以在其中看到旧键或新键。