这一节笼统地描述了 Python
如何用散列表来实现 dict
类型,有些细节只是一笔带过。但是总体来说描述是准确的。
散列表
其实是一个稀疏数组
(总是有空白
元素的数组称为稀疏数组
)。在一般的数据结构教材中,散列表里的单元
通常叫作表元
(bucket
)。在 dict
的散列表当中,每个键值
对都占用一个表元
,每个表元都有两个部分
,一个是对键的引用
,另一个是对值的引用
。因为所有表元的大小一致
,所以可以通过偏移量
来读取某个表元。
因为 Python
会设法保证大概还有三分之一
的表元
是空的
,所以在快要达到这个阈值
的时候,原有的散列表会被复制
到一个更大的空间里面。
如果要把一个对象
放入散列表
,那么首先要计算这个元素键
的散列值
。Python
中可以用hash()
方法来做这件事情,接下来会介绍这一点。
散列值和相等性
内置的 hash()
方法可以用于所有
的内置类型对象
。如果是自定义
对象调用 hash()
的话,实际上运行的是自定义的 __hash__
。如果两个对象
在比较的时候是相等的
,那它们的散列值
必须相等
,否则散列表就不能正常运行了。例如,如果 1 == 1.0
为真,那么 hash(1) == hash(1.0)
也必须为真,但其实这两个数字(整型和浮点
)的内部结构
是完全不一样
的。
为了让散列值
能够胜任散列表索引
这一角色,它们必须在索引空间
中尽量分散开来。这意味着在最理想的状况下,越是相似但不相等的对象,它们散列值的差别应该越大。下面示例是一段代码输出,这段代码被用来比较散列值的二进制表达的不同。注意其中 1
和1.0
的散列值是相同的,而 1.0001
、1.0002
和 1.0003
的散列值则非常不同
。
tips:
从 Python 3.3
开始,str
、bytes
和 datetime
对象的散列值计算过程中多了随机
的“加盐
”这一步。所加盐值是 Python
进程内的一个常量,但是每次启动Python
解释器都会生成一个不同的盐值
。随机盐值的加入是为了防止 DOS
攻击而采取的一种安全措施。在 __hash__
特殊方法的文档(https://docs.python.org/3/reference/datamodel.html#object.__hash__
) 里有相关的详细信息。
了解对象散列值相关的基本概念之后,我们可以深入到散列表工作原理背后的算法了。
散列表算法
为了获取 my_dict[search_key]
背后的值,Python
首先会调用 hash(search_key)
来计算search_key
的散列值,把这个值最低
的几位
数字当作偏移量
,在散列表
里查找
表元(具体取几位,得看当前散列表的大小)。若找到的表元是空
的,则抛出 KeyError
异常。若不是空的,则表元里会有一对 found_key:found_value
。这时候 Python
会检验 search_key == found_key
是否为真,如果它们相等的话,就会返回 found_value
。
如果