diff --git "a/src/40.\345\255\227\345\205\270\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\214\345\256\203\347\232\204\345\272\225\345\261\202\347\273\223\346\236\204\351\225\277\344\273\200\344\271\210\346\240\267\345\255\220\357\274\237.md" "b/src/40.\345\255\227\345\205\270\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\214\345\256\203\347\232\204\345\272\225\345\261\202\347\273\223\346\236\204\351\225\277\344\273\200\344\271\210\346\240\267\345\255\220\357\274\237.md"
index 9bf61f7..ca7c634 100644
--- "a/src/40.\345\255\227\345\205\270\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\214\345\256\203\347\232\204\345\272\225\345\261\202\347\273\223\346\236\204\351\225\277\344\273\200\344\271\210\346\240\267\345\255\220\357\274\237.md"
+++ "b/src/40.\345\255\227\345\205\270\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\214\345\256\203\347\232\204\345\272\225\345\261\202\347\273\223\346\236\204\351\225\277\344\273\200\344\271\210\346\240\267\345\255\220\357\274\237.md"
@@ -99,16 +99,16 @@ typedef struct _dictkeysobject PyDictKeysObject;
// Objects/dict-common.h
struct _dictkeysobject {
// key 的引用计数,也就是 key 被多少个字典所使用
- // 如果是结合表,那么该成员始终是 1,因为结合表独占一组 key
- // 如果是分离表,那么该成员大于等于 1,因为分离表可以共享一组 key
+ // 如果是结合表,那么该字段始终是 1,因为结合表独占一组 key
+ // 如果是分离表,那么该字段大于等于 1,因为分离表可以共享一组 key
Py_ssize_t dk_refcnt;
// 哈希表的大小、或者说长度,注意:dk_size 满足 2 的 n 次方
- // 这样可将模运算优化成按位与运算,也就是将 num % dk_size 替换成 num & (dk_size - 1)
+ // 这样可将取模运算优化成按位与运算,也就是将 num % dk_size 优化成 num & (dk_size - 1)
Py_ssize_t dk_size;
// 哈希函数,用于计算 key 的哈希值,然后映射成索引
- // 一个好的哈希函数应该能尽量少的避免冲突,并且哈希函数对哈希表的性能起着至关重要的作用
+ // 一个好的哈希函数应该尽可能少的产生冲突,并且哈希函数对哈希表的性能起着至关重要的作用
// 所以底层的哈希函数有很多种,会根据对象的种类选择最合适的一个
dict_lookup_func dk_lookup;
@@ -150,7 +150,7 @@ typedef struct {
字典维护的键值对(entry)会按照先来后到的顺序保存在键值对数组中,而哈希索引数组则保存键值对在键值对数组中的索引。另外,哈希索引数组中的一个位置我们称之为一个槽,比如图中的哈希索引数组便有 8 个槽,其数量由 dk_size 字段维护。
-比如我们创建一个空字典,注意:虽然字典是空的,但是容量已经有了,然后往里面插入键值对 "komeiji": 99 的时候,Python 会执行以下步骤:
+假设我们创建一个空字典,注意:虽然字典是空的,但是容量已经有了,然后往里面插入键值对 "komeiji": 99 的时候,Python 会执行以下步骤:
+ 将键值对保存在 dk_entries 中,由于初始字典是空的,所以会保存在 dk_entries 数组中索引为 0 的位置。
+ 通过哈希函数计算出 "komeiji" 的哈希值,然后将哈希值映射成索引,假设是 6。
@@ -233,6 +233,7 @@ typedef struct {
+ 哈希表本质上就是个数组,只不过 Python 选择使用两个数组实现,其中哈希索引数组的长度便是哈希表的容量,而该长度由 dk_size 字段维护。
+ 由于哈希表最多使用 2/3,那么就只为键值对数组申请 2/3 容量的空间。对于容量为 8 的哈希表,那么哈希索引数组的长度就是 8,键值对数组的长度就是 5。
+ dk_usable 字段表示键值对数组还可以容纳的 entry 的个数,所以它的初始值也是 5。
++ dk_nentries 字段表示当前已存在的 entry 的数量,假设哈希表,或者说键值对数组存储了 3 个键值对,那么 dk_nentries 就是 3。而 dk_usable 则会变成 5 - 3 等于 2,因为它表示键值对数组还可以容纳多少 entry。
咦,前面介绍 PyDictObject 的时候,看到里面有一个 ma_used 字段,表示字典的长度。那么 dk_nentries 和 ma_used 有啥区别呢,从字面意思上看,两者的含义貌似是等价的,关于这一点后续再解释。
@@ -248,7 +249,7 @@ typedef struct {
![](./images/113.png)
-早期的哈希表只有一个键值对数组,键值对在存储时本身就是无序的,那么遍历的结果自然也是无序的。对于当前来说,遍历的结果就是 "b"、"a"、"c"。
+早期的哈希表只有一个键值对数组,而键值对在存储时本身就是无序的,那么遍历的结果自然也是无序的。对于当前来说,遍历的结果就是 "b"、"a"、"c"。
但从 3.6 开始,键值对数组中的键值对,和添加顺序是一致的。而遍历时,会直接遍历键值对数组,因此遍历的结果是有序的。对于当前来说,遍历的结果就是 "a"、"b"、"c"。
@@ -286,7 +287,7 @@ struct _dictkeysobject {
// 那么 PyDictKeysObject 实例占 40 个字节
~~~
-然后是剩余的两个数组,一个是 char 类型的数组 dk_indices,里面 1 个元素占 1 字节;还有一个 PyDictKeyEntry 类型的数组 dk_entries,里面一个元素占 24 字节。所以对于容量为 n 的字典来说:
+然后是剩余的两个数组,一个是哈希索引数组 dk_indices,里面 1 个元素可能占 1 字节、2 字节、或 4 字节;还有一个键值对数组 dk_entries,里面一个元素占 24 字节。所以对于容量为 n 的字典来说:
+ 如果 n < 256,字典大小等于 48 + 40 + n + n \* 2 // 3 \* 24
+ 如果 256 <= n < 65536,字典大小等于 48 + 40 + n \* 2 + n \* 2 // 3 \* 24