用户定义类的默认哈希是返回它们的 id。这提供了通常很有用的行为;使用用户定义类的实例作为字典键将允许在再次提供完全相同对象以查找值时检索关联值。例如:
>>> class Foo(object):
def __init__(self, foo):
self.foo = foo
>>> f = Foo(10)
>>> d = {f: 10}
>>> d[f]
10
这与用户定义类的默认相等性匹配:
>>> g = Foo(10)
>>> f == g
False
>>> d[g]
Traceback (most recent call last):
File "<pyshell#9>", line 1, in <module>
d[g]
KeyError: <__main__.Foo object at 0x0000000002D69390>
注意,尽管
f
和
g
的属性具有相同的值,它们并不相等,查找
d
中的
g
并不能找到存储在
f
下的值。此外,即使我们更改了
f.foo
的值,在
d
中查找
f
仍然可以找到该值。
>>> f.foo = 11
>>> d[f]
10
假设某个新类的实例在没有程序员特别声明其等效条件的情况下应被视为不等效,这可以通过定义__eq__
和__hash__
来实现。
这种方法非常有效;如果我定义一个Car
类,那么具有相同属性的两辆汽车可能代表着两辆不同的汽车。如果我有一个映射汽车到注册所有者的字典,我不希望在查找Bob的汽车时找到Alice,即使Alice和Bob恰好拥有相同的汽车!另一方面,如果我定义了一个表示邮政编码的类,则可能确实希望将具有相同代码的两个不同对象视为可互换的"同一"事物的表示。在这种情况下,如果我有一个将邮政编码映射到州的字典,我显然希望能够使用表示相同邮政编码的两个不同对象找到相同的州。
我将其称为“值类型”和“对象类型”之间的区别。值类型表示某个值,我关心的是该值,而不是每个单独对象的标识。用两种不同的方式得到相同值都是同样好的,并且围绕值类型传递代码的"契约"通常只承诺提供一个具有某些值的对象,而不指定哪个特定对象。对于对象类型,则每个单独实例都有自己的标识符,即使它包含与另一个实例完全相同的数据。围绕对象类型传递代码的"契约"通常承诺跟踪确切的单个对象。
那么为什么内置的可变类不使用其id作为其哈希值?因为它们都是容器,我们通常认为容器在大多数情况下类似于值类型,其值由所包含的元素确定:
>>> [1, 2, 3] == [1, 2, 3]
True
>>> {f: 10} == {f: 10}
True
但是,可变容器具有短暂的值。某个给定列表当前具有值[1, 2, 3]
,但它可以被改变为具有值[4, 5, 6]
。如果您可以将列表用作字典键,则我们必须对查找是否应使用列表的(当前)值或其标识进行裁决。无论哪种方式,当正在用作字典键的对象的值通过突变而发生变化时,我们都可能会感到(非常)惊讶。仅当对象的值是其标识或对象的标识与其价值无关时,将对象用作字典键才能很好地工作。因此,Python选择声明不可哈希的可变容器。
现在,针对您直接提出的问题的更具体细节:
1)由于在CPython中,默认哈希映射到对象的内存地址(尽管根据其他答案/评论,似乎只有<2.6),因此在同时存在的两个使用默认散列的对象上,无论涉及哪些类,它们的哈希值都不可能产生冲突(如果它作为字典键存储,它就是活动的)。我还期望其他不使用内存地址作为哈希的Python实现在使用默认哈希时应该仍然具有良好的哈希分布。所以是的,你可以依靠它。
2)只要您不返回作为自定义哈希的结果恰好是某个现有对象的哈希值,您就应该相对安全。我的理解是,Python基于哈希的容器对次优哈希函数相对宽容,只要它们不完全退化。
__hash__
,也要重写__eq__
”。 - John Strood