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