3.9.2 字典中的散列表
这一节笼统地描述了 Python 如何用散列表来实现 dict 类型,有些细节 只是一笔带过,像 CPython 里的一些优化技巧 8 就没有提到。但是总体 来说描述是准确的。
8Python 源码 dictobject.c 模块(http://hg.python.org/cpython/file/tip/Objects/dictobject.c)里有丰 富的注释,另外延伸阅读中有对《代码之美》一书的引用。
为了简单起见,这里先集中讨论 dict 的内部结构,然后再延 伸到集合上面。
散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。 在一般的数据结构教材中,散列表里的单元通常叫作表元(bucket)。 在 dict 的散列表当中,每个键值对都占用一个表元,每个表元都有两 个部分,一个是对键的引用,另一个是对值的引用。因为所有表元的大 小一致,所以可以通过偏移量来读取某个表元。
因为 Python 会设法保证大概还有三分之一的表元是空的,所以在快要达 到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。
如果要把一个对象放入散列表,那么首先要计算这个元素键的散列值。 Python 中可以用 hash() 方法来做这件事情,接下来会介绍这一点。
01. 散列值和相等性
内置的 hash() 方法可以用于所有的内置类型对象。如果是自定义 对象调用 hash() 的话,实际上运行的是自定义的 __hash__。如 果两个对象在比较的时候是相等的,那它们的散列值必须相等,否 则散列表就不能正常运行了。例如,如果 1 == 1.0 为真,那么 hash(1) == hash(1.0) 也必须为真,但其实这两个数字(整型 和浮点)的内部结构是完全不一样的。9
为了让散列值能够胜任散列表索引这一角色,它们必须在索引空间 中尽量分散开来。这意味着在最理想的状况下,越是相似但不相等 的对象,它们散列值的差别应该越大。示例 3-16 是一段代码输 出,这段代码被用来比较散列值的二进制表达的不同。注意其中 1 和 1.0 的散列值是相同的,而 1.0001、1.0002 和 1.0003 的散列值则 非常不同。
示例 3-16 在32 位的 Python 中,1、1.0001、1.0002 和 1.0003 这几个数的散列值的二进制表达对比(上下两个二进制间不同 的位被 ! 高亮出来,表格的最右列显示了有多少位不相同)
用来计算示例 3-16 的程序见于附录 A。尽管程序里大部分代码都 是用来整理输出格式的,考虑到完整性,我还是把全部的代码放在 示例 A-3 中了。
从 Python 3.3 开始,str、bytes 和 datetime 对象的散 列值计算过程中多了随机的“加盐”这一步。所加盐值是 Python 进程内的一个常量,但是每次启动 Python 解释器都会生成一个 不同的盐值。随机盐值的加入是为了防止 DOS 攻击而采取的 一种安全措施。在 __hash__ 特殊方法的文档 (https://docs.python.org/3/reference/datamodel.html#object.__hash__) 里有相关的详细信息。
了解对象散列值相关的基本概念之后,我们可以深入到散列表工作 原理背后的算法了。
02. 散列表算法
为了获取 my_dict[search_key] 背后的值,Python 首先会调用 hash(search_key) 来计算 search_key 的散列值,把这个值最低 的几位数字当作偏移量,在散列表里查找表元(具体取几位,得看 当前散列表的大小)。若找到的表元是空的,则抛出 KeyError 异 常。若不是空的,则表元里会有一对 found_key:found_value。 这时候 Python 会检验 search_key == found_key 是否为真,如 果它们相等的话,就会返回 found_value。
如果 search_key 和 found_key 不匹配的话,这种情况称为散列 冲突。发生这种情况是因为,散列表所做的其实是把随机的元素映 射到只有几位的数字上,而散列表本身的索引又只依赖于这个数字 的一部分。为了解决散列冲突,算法会在散列值中另外再取几位, 然后用特殊的方法处理一下,把新得到的数字再当作索引来寻找表 元。10 若这次找到的表元是空的,则同样抛出 KeyError;若非 空,或者键匹配,则返回这个值;或者又发现了散列冲突,则重复 以上的步骤。图 3-3 展示了这个算法的示意图。
posted on
浙公网安备 33010602011771号