为什么 Python 字典可以有多个具有相同哈希值的键?

2025-03-13 09:22:00
admin
原创
11
摘要:问题描述:我正在尝试了解 Pythonhash函数的底层原理。我创建了一个自定义类,其中所有实例都返回相同的哈希值。class C: def __hash__(self): return 42 我只是假设在任何时候上述类只有一个实例可以存在于 a 中dict,但实际上 adict可以有...

问题描述:

我正在尝试了解 Pythonhash函数的底层原理。我创建了一个自定义类,其中所有实例都返回相同的哈希值。

class C:
    def __hash__(self):
        return 42

我只是假设在任何时候上述类只有一个实例可以存在于 a 中dict,但实际上 adict可以有多个具有相同哈希值的元素。

c, d = C(), C()
x = {c: 'c', d: 'd'}
print(x)
# {<__main__.C object at 0x7f0824087b80>: 'c', <__main__.C object at 0x7f0823ae2d60>: 'd'}
# note that the dict has 2 elements

我进行了一些实验,发现如果我重写该__eq__方法,使得该类的所有实例都相等,那么dict只允许一个实例。

class D:
    def __hash__(self):
        return 42
    def __eq__(self, other):
        return True

p, q = D(), D()
y = {p: 'p', q: 'q'}
print(y)
# {<__main__.D object at 0x7f0823a9af40>: 'q'}
# note that the dict only has 1 element

所以我很好奇想知道如何dict可以拥有具有相同哈希值的多个元素。


解决方案 1:

这是我能够整理的有关 Python 字典的所有内容(可能比任何人想知道的都多;但答案很全面)。感谢Duncan指出 Python 字典使用插槽并引导我进入这个兔子洞。

  • Python 字典以哈希表的形式实现。

  • 哈希表必须允许哈希冲突,即,即使两个键具有相同的哈希值,表的实现也必须具有一种策略来明确地插入和检索键和值对。

  • Python dict 使用开放寻址来解决哈希冲突(如下所述)(参见dictobject.c:296-297)。

  • Python 哈希表只是一块连续的内存块(有点像数组,因此您可以O(1)通过索引进行查找)。

  • 表中的每个槽位只能存储一个条目。这一点很重要

  • 表中的每个条目实际上是三个值的组合 -. 这是作为 C 结构实现的(参见dictobject.h:51-56)

  • 下图是 Python 哈希表的逻辑表示。下图中,左侧的 0、1、...、i、... 是哈希表中的索引(它们仅用于说明目的,显然不与表一起存储!)。

# Logical model of Python Hash table
-+-----------------+
0| <hash|key|value>|
-+-----------------+
1|      ...        |
-+-----------------+
.|      ...        |
-+-----------------+
i|      ...        |
-+-----------------+
.|      ...        |
-+-----------------+
n|      ...        |
-+-----------------+
  • 当初始化一个新的字典时,它以 8 个开始。(参见dictobject.h:49)

  • 在向表中添加条目时,我们从一些i基于键的哈希值的插槽开始。CPython 使用 initial i = hash(key) & mask。Where mask = PyDictMINSIZE - 1,但这并不重要)。只需注意,检查的初始插槽 i 取决于键的哈希值。

  • 如果该插槽为空,则将条目添加到插槽中(我指的是条目<hash|key|value>)。但是如果该插槽已被占用怎么办!?很可能是因为另一个条目具有相同的哈希值(哈希冲突!)

  • 如果插槽已被占用,CPython(甚至 PyPy)会将插槽中条目的哈希值和键(比较是指==比较而不是比较)与要插入的当前条目的键( dictobject.c:337、344-345)进行比较。如果两者匹配,则它会认为该条目已经存在,放弃并转到下一个要插入的条目。如果哈希值或键不匹配,它会开始探测is

  • 探测只是意味着它会逐个搜索槽位以找到一个空槽位。从技术上讲,我们可以一个接一个地搜索,i+1、i+2......并使用第一个可用的槽位(这是线性探测)。但由于注释中详细解释的原因(参见dictobject.c:33-126),CPython 使用随机探测。在随机探测中,下一个槽位以伪随机顺序挑选。该条目将添加到第一个空槽位。对于本次讨论,选择下一个槽位的实际算法并不重要(有关探测算法,请参阅dictobject.c:33-126)。重要的是探测槽位,直到找到第一个空槽位。

  • 查找也会发生同样的事情,只是从初始槽位 i 开始(其中 i 取决于密钥的哈希值)。如果哈希值和密钥都不匹配槽位中的条目,它就会开始探测,直到找到匹配的槽位。如果所有槽位都已用尽,它会报告失败。

  • 顺便说一句,如果字典已满三分之二,则会调整大小。这可以避免减慢查找速度。(请参阅dictobject.h:64-65)

就是这样!Python 的 dict 实现在插入项目时会检查两个键的哈希相等性和==键的正常相等性 ()。因此,总而言之,如果有两个键,abhash(a)==hash(b),但a!=b,那么它们可以和谐地存在于 Python dict 中。但如果hash(a)==hash(b) a==b,那么它们就不能同时存在于同一个 dict 中。

因为我们必须在每次哈希碰撞之后进行探测,所以过多哈希碰撞的一个副作用是查找和插入会变得非常慢(正如 Duncan 在评论中指出的那样)。

我想对我的问题的简短回答是“因为这就是它在源代码中的实现方式;)”

虽然知道这一点很好(为了获得极客积分?),但我不确定如何在现实生活中使用它。因为除非你试图明确破坏某些东西,否则为什么两个不相等的对象会有相同的哈希值?

解决方案 2:

有关 Python 散列工作原理的详细描述,请参阅我的回答“为什么早期返回比其他方法慢?”

基本上,它使用哈希值在表中选择一个位置。如果位置中有值并且哈希值匹配,它会比较项目以查看它们是否相等。

如果哈希匹配但项不相等,则它会尝试另一个槽位。有一个公式可以做到这一点(我在参考答案中描述了这一点),它会逐渐拉入哈希值中未使用的部分;但是一旦它用完了所有部分,它最终会遍历哈希表中的所有槽位。这保证了我们最终要么找到匹配项,要么找到一个空槽位。当搜索找到一个空槽位时,它会插入值或放弃(取决于我们是添加还是获取值)。

需要注意的重要一点是,没有列表或存储桶:只有一个具有特定数量槽的哈希表,并且每个哈希用于生成候选槽序列。

解决方案 3:

编辑:下面的答案是处理哈希冲突的可能方法之一,但这不是Python处理它的方式。下面引用的 Python wiki 也是不正确的。@Duncan 下面给出的最佳来源是实现本身:https://github.com/python/cpython/blob/master/Objects/dictobject.c我为混淆道歉。


它在哈希中存储元素列表(或存储桶),然后遍历该列表,直到找到列表中的实际键。一张图片胜过千言万语:

哈希表

在这里你可以看到John SmithSandra Dee都哈希到152。桶152包含它们两个。查找时,Sandra Dee它首先在桶中找到列表152,然后循环遍历该列表,直到Sandra Dee找到并返回521-6955

以下内容是错误的,仅用于上下文:在Python 的 wiki上,您可以找到 Python 如何执行查找的(伪?)代码。

实际上,这个问题有几种可能的解决方案,请查看维基百科文章以获得更好的概述:http://en.wikipedia.org/wiki/Hash_table#Collision_resolution

解决方案 4:

哈希表通常必须允许哈希冲突!您可能会不走运,两个东西最终会哈希到同一个东西。在下面,项目列表中有一组具有相同哈希键的对象。通常,该列表中只有一个东西,但在这种情况下,它会继续将它们堆叠到同一个东西中。它知道它们不同的唯一方法是通过等号运算符。

当发生这种情况时,您的性能会随着时间的推移而下降,这就是为什么您希望哈希函数尽可能“随机”。

解决方案 5:

在线程中,我没有看到当我们将用户定义类的实例作为键放入字典中时,python 究竟如何处理这些实例。让我们阅读一些文档:它声明只有可哈希对象才能用作键。可哈希的都是不可变的内置类和所有用户定义类。

用户定义的类默认具有 __cmp__() 和 __hash__() 方法;使用它们,所有对象比较不相等(与它们自身除外)并且 x.__hash__() 返回从 id(x) 派生的结果。

因此,如果您的类中有一个常量 hash__,但没有提供任何 __cmpeq 方法,则您的所有实例对于字典而言都是不平等的。另一方面,如果您提供任何 cmpeq 方法,但没有提供 __hash__,您的实例在字典方面仍然不平等。

class A(object):
    def __hash__(self):
        return 42


class B(object):
    def __eq__(self, other):
        return True


class C(A, B):
    pass


dict_a = {A(): 1, A(): 2, A(): 3}
dict_b = {B(): 1, B(): 2, B(): 3}
dict_c = {C(): 1, C(): 2, C(): 3}

print(dict_a)
print(dict_b)
print(dict_c)

输出

{<__main__.A object at 0x7f9672f04850>: 1, <__main__.A object at 0x7f9672f04910>: 3, <__main__.A object at 0x7f9672f048d0>: 2}
{<__main__.B object at 0x7f9672f04990>: 2, <__main__.B object at 0x7f9672f04950>: 1, <__main__.B object at 0x7f9672f049d0>: 3}
{<__main__.C object at 0x7f9672f04a10>: 3}
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1579  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1355  
  信创产品在政府采购中的占比分析随着信息技术的飞速发展以及国家对信息安全重视程度的不断提高,信创产业应运而生并迅速崛起。信创,即信息技术应用创新,旨在实现信息技术领域的自主可控,减少对国外技术的依赖,保障国家信息安全。政府采购作为推动信创产业发展的重要力量,其对信创产品的采购占比情况备受关注。这不仅关系到信创产业的发展前...
信创和国产化的区别   8  
  信创,即信息技术应用创新产业,旨在实现信息技术领域的自主可控,摆脱对国外技术的依赖。近年来,国货国用信创发展势头迅猛,在诸多领域取得了显著成果。这一发展趋势对科技创新产生了深远的推动作用,不仅提升了我国在信息技术领域的自主创新能力,还为经济社会的数字化转型提供了坚实支撑。信创推动核心技术突破信创产业的发展促使企业和科研...
信创工作   9  
  信创技术,即信息技术应用创新产业,旨在实现信息技术领域的自主可控与安全可靠。近年来,信创技术发展迅猛,对中小企业产生了深远的影响,带来了诸多不可忽视的价值。在数字化转型的浪潮中,中小企业面临着激烈的市场竞争和复杂多变的环境,信创技术的出现为它们提供了新的发展机遇和支撑。信创技术对中小企业的影响技术架构变革信创技术促使中...
信创国产化   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用