用户定义的类的默认哈希是只返回它们的 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
具有相同的属性值,它们也不相等,并且在查找g
中d
找不到存储在 下的值f
。此外,即使我们更改 的值,f.foo
查找仍然会找到该值:f
d
>>> f.foo = 11
>>> d[f]
10
__eq__
假设是一些任意新类的实例应该被视为非等价的,除非程序员通过定义和明确声明两个实例被视为等价的条件__hash__
。
这非常有效;如果我定义一个Car
类,我可能会认为两辆具有相同属性的汽车代表两辆不同的汽车。如果我有一本将汽车映射到注册车主的字典,我不想在查找 Bob 的汽车时找到 Alice,即使 Alice 和 Bob 碰巧拥有相同的汽车!OTOH,如果我定义一个类来表示邮政编码,我可能确实想考虑两个具有相同代码的不同对象是“相同”事物的可互换表示,在这种情况下,如果我有一个将邮政编码映射到州的字典,我显然希望能够找到具有表示相同邮政编码的两个不同对象的相同状态。
我将此称为“值类型”和“对象类型”之间的区别。值类型代表一些值,这是我关心的值,而不是每个单独对象的身份。产生相同值的两种不同方式同样好,并且围绕值类型传递的代码的“合同”通常只是承诺为您提供具有某些值的对象,而不指定它是哪个特定对象。对于对象类型 OTOH,每个单独的实例都有自己的标识,即使它包含与另一个实例完全相同的数据。围绕对象类型传递的代码“契约”通常承诺跟踪确切的单个对象。
那么为什么内置的可变类不使用它们的 id 作为它们的哈希呢?这是因为它们都是容器,我们通常认为容器大多类似于值类型,它们的值由包含的元素决定:
>>> [1, 2, 3] == [1, 2, 3]
True
>>> {f: 10} == {f: 10}
True
但是可变容器的值是瞬态的。一些给定的列表当前具有该值[1, 2, 3]
,但它可以被变异为具有该值[4, 5, 6]
。如果您可以使用列表作为字典键,那么我们必须就查找是否应该使用列表的(当前)值或其标识做出裁决。无论哪种方式,当当前用作字典键的对象的值通过变异来更改时,我们都会(非常)惊讶。仅当对象的值是它的标识,或者对象的标识与其值无关时,将对象用作字典键才有效。所以 Python 选择的答案是声明可变容器不可散列。
现在,回答您的直接问题的更具体细节:
1)由于CPython中的这个默认散列(虽然显然只有<2.6,根据其他答案/评论)映射到对象的内存地址,所以在CPython中,没有两个同时使用默认散列的对象可能会发生冲突它们的哈希值,不管涉及的类是什么(如果它被存储为字典键,它就是实时的)。我还希望其他不使用内存地址作为散列的 Python 实现仍然应该在使用默认散列的对象之间具有良好的散列分布。所以是的,你可以依靠它。
2)只要您不返回与某个现有对象的哈希值完全相同的结果作为您的自定义哈希值,您就应该相对没问题。我的理解是,Python 的基于散列的容器相对可以容忍次优散列函数,只要它们没有完全退化。