6.0 序

元素和元素之间可能存在着某种关系,比如学生姓名和成绩。我希望能够通过学生的姓名找到这个学生的成绩,那么只需要将两者关联起来即可。字典正是这么做的,字典中的每个元素就是一个key:value键值对,通过指定的key可以找到value。首先我们在前面的章节中说过,字典这种数据结构,python底层也在大量的使用,比如每一个类都有自己的属性字典,这就意味着python对字典这种数据结构的性能要求是极其苛刻的。所以在python底层,对字典这种数据结构进行了高度的优化。理论上,字典查找元素的时间复杂度是O(1)。

字典底层对应的结构体是PyDictObject,其实我不说,也能猜出来。再比如set,那么底层对应的结构体显然是PySetObject。我们先不看PyDictObject,我们来想一想为什么字典的查找效率是O(1),它底层是使用了什么原理。

6.1 哈希表

我们在tuple那一章中提到了哈希,还说tuple可以作为字典的key,list不可以,就是因为list是不可哈希的。没错,dict底层正是使用了哈希表,哈希表也叫做散列表。它是将值通过hash运算转为一个数值,这个数值来充当索引。这样解释可能会让人很迷,我们来具体看一张图。

我们发现除了key、value之外,还有一个index。其实hash表本质上也是使用了索引的思想,会把这个key通过函数映射成一个数值,作为索引。至于是怎么映射的,可以的话后面再谈,现在我们就假设是按照我们接下来说的方法映射的。

比如我们这里有一个能容纳10个元素的字典,我们先设置d["satori"]=82,那么会对satori这个字符串进行一个哈希运算,然后再对10、也就是当前的总容量取模,这样的是不是能够得到一个小于10的数呢?假设是5,那么就存在索引为5地方。然后又进行d["koishi"]=83,那么按照同样的规则运算得到8,那么就存在索引为8的位置,同理第三次设置d["mashiro"]=80,对mashiro进行哈希、取模,得到2,那么存储在索引为2的地方。

同理,当我们取值的时候,取d["satori"],那么同样会对satori进行哈希、取模,得到索引,发现是5,直接把索引为5的value给取出来。当然这种说法肯定是不严谨的,为什么我们来想一个问题。

  • 哈希、取模运算之后得到的结果一定是不同的吗?
  • 在运算得到索引的时候,发现这个位置已经有人占了怎么办?
  • 取值的时候,索引为5,可如果索引为5对应的key和我们指定获取的key不一致怎么办?

哈希值是有冲突的,如果一旦冲突,那么python底层会改变算法继续映射,直到映射出来的索引没有人用。比如我们设置一个新的key、value,d["tomoyo"]=88,可是我们对tomoyo这个key进行映射之后得到的结果也是5,而索引为5的地方已经被key=satori的键值对给占了,那么python就会换一种规则来对tomoyo进行hash运算,然后添加进去。但如果我们再次设置d["satori"]=100,那么对satori进行映射得到的结果也是5,而key是一致的,那么就会把对应的值进行修改。

同理,当我们获取值的时候,d["tomoyo"],对key进行映射,得到索引,但是发现key不是我们指定的key,于是改变规则(这个规则跟设置值冲突时,采用的规则是一样的),重新映射,得到索引,然后发现key是一致的,于是将值取出来。

但如果我们指定了一个不存在的key,那么哈希映射,找到对应索引,发现没有key,证明我们指定的key是不存在的。但如果有的话,发现key和我们指定的key不相等,说明我们只是碰巧撞上了,但由于key不一样,因此会改变规则重新运算,得到新的索引,发现没有对应的key,于是报错:指定的key不存在。

所以从这里就已经能说明问题了,就是把key转换成类似列表的索引。可能有人问,这些值貌似不是连续的啊,对的,肯定不是连续的。并不是说你先存,你的索引就小、就在前面,这是由key进行hash映射之后的结果决定的。而且容量有10个,目前我们只存了4个元素,那么哈希表、或者说字典会不会扩容呢?当然,既然是可变对象,当然会扩容。并且它还不是像列表那样,容量不够才扩容,而当元素个数达到容量的三分之二的时候就会扩容。

我们可以认为字典底层还是使用了索引的思想,字典不可能会像列表那样,元素之间是连续的,一个一个挨在一起的。既然是哈希运算,得到的值肯定是随机的。容量为10,尽管有6个是空着的,但是没关系,我只要保证我设置的元素整体上是有序的即可。就好比有10张桌椅,小红坐在第3张,小明坐在第8张,尽管有空着的,但是没关系,就让它空着。只要我到第3张桌椅能够找到小红、第8张可以找到小明即可。这些桌椅就可以看成是索引,只要我通过索引能够找到对应的元素即可。但是容量为10,为什么不能全部占满之后再扩容呢?试想一下,既然是随机的,那么肯定会出现哈希值碰撞,并且当元素个数到达三分之二之后,这种碰撞的概率非常大。因此当容量到达三分之二的时候,就会申请一份更大的空间,以便来容纳新的元素。

所以我们发现哈希表实际上就是一种空间换时间的方法,如果容量为100,那么就相当于有100个位置,每个元素都进行哈希映射,找到自己的位置。各自的位置都是不固定的,也许会空出来很多元素,但是无所谓,只要保证这些元素在100个位置上是相对有序、通过哈希运算得到索引之后,可以在相应的位置找到它即可。

所以相信应该所有人都能明白为什么哈希表的时间复杂度是O(1)了,就实际因为转化成了索引,每一个索引都是连续的,只不过一部分索引没有相应的key、value罢了。但这无所谓,因为索引和key、value是一一对应的,通过索引我们能瞬间定位到指定的key,再来检测key是否存在以及和我们指定的key是否一致。如果不存在,那么不好意思,证明这个地方根本没有key、value,说明我们指定了一个不存在的key。而且由于元素个数达到容量的三分之二的时候,碰撞的概率非常大,因此几乎不可能出现容量正好都排满的情况,否则那要改变规则、重复映射多少次啊。

一句话总结:哈希表就是一种空间换时间的方法

关于哈希表设置元素、和获取元素用流程图表示的话,就是:

6.2 PyDictObject对象

字典中的一个key、value,我们在底层会把它称之为一个entry,至于为什么?我们后面在源码中可以看到

  1. typedef struct _dictkeysobject PyDictKeysObject;
  2. /* The ma_values pointer is NULL for a combined table
  3. * or points to an array of PyObject* for a split table
  4. 对于一张combined table,ma_values指针为NULL
  5. 对于一张split table,则指向一个数组,数组里面都是PyObject *
  6. */
  7. typedef struct {
  8. //注意这是PyObject_HEAD,不是PyObject_VAR_HEAD
  9. //PyObject_HEAD只有引用计数和类型,没有ob_size
  10. PyObject_HEAD
  11. //字典里面元素的个数,active
  12. Py_ssize_t ma_used;
  13. /* Dictionary version: globally unique, value change each time
  14. the dictionary is modified
  15. 字典版本:全局唯一,每一次value的变动,都会导致其改变
  16. */
  17. uint64_t ma_version_tag;
  18. /*
  19. 如果ma_values为NULL,这是一张combined table,所有的key和value都存在ma_keys里面
  20. */
  21. PyDictKeysObject *ma_keys;
  22. /*
  23. 如果ma_values不为NULL,这是一张split table,那么key都存在ma_keys里
  24. 所有的values都存在ma_values这个数组里
  25. */
  26. PyObject **ma_values;
  27. } PyDictObject;
  28. //不管装在设么地方,我们看到存储的都是PyObject *
  29. //说明字典是什么都可以装的(不可变类型)

但是说实话,直接这么看是很难看懂的,然而我们发现有一个PyDictKeysObject *,而这个家伙就是_dictkeysobject,从最上面的typedef struct也能看出来,我们来看看这个_dictkeysobject是什么吧

  1. //Objects/dict-common.h
  2. struct _dictkeysobject {
  3. //引用计数
  4. Py_ssize_t dk_refcnt;
  5. /* Size of the hash table (dk_indices). It must be a power of 2. */
  6. /* 哈希表的大小,必须是2的倍数 */
  7. Py_ssize_t dk_size;
  8. /* 与哈希表有关的函数 */
  9. dict_lookup_func dk_lookup;
  10. /* Number of usable entries in dk_entries. */
  11. /* dk_entries中可用的entries数量 */
  12. Py_ssize_t dk_usable;
  13. /* Number of used entries in dk_entries. */
  14. /* dk_entries中已经使用的entries数量 */
  15. Py_ssize_t dk_nentries;
  16. /* Actual hash table of dk_size entries. It holds indices in dk_entries,
  17. or DKIX_EMPTY(-1) or DKIX_DUMMY(-2).
  18. Indices must be: 0 <= indice < USABLE_FRACTION(dk_size).
  19. The size in bytes of an indice depends on dk_size:
  20. Dynamically sized, SIZEOF_VOID_P is minimum. */
  21. //最终的哈希表,它存储了dk_entries的索引
  22. //里面的类型是会随着dk_size的大小而变化的
  23. /*
  24. - 1 byte if dk_size <= 0xff (char*)
  25. - 2 bytes if dk_size <= 0xffff (int16_t*)
  26. - 4 bytes if dk_size <= 0xffffffff (int32_t*)
  27. - 8 bytes otherwise (int64_t*)
  28. */
  29. char dk_indices[]; /* char is required to avoid strict aliasing. */
  30. /* "PyDictKeyEntry dk_entries[dk_usable];" array follows:
  31. see the DK_ENTRIES() macro */
  32. };
  33. //我们一直提到了dk_entries,这又是个啥?
  34. //dk_entries是一个数组,里面的元素类型是PyDictKeyEntry,就是一个一个的键值对
  35. //所以我们把某个键值对称之为一个entry,它的大小可以用USABLE_FRACTION这个宏来获取
  36. typedef struct {
  37. /* me_key的哈希值,避免每次查询的时候都要重新建立 */
  38. Py_hash_t me_hash;
  39. //字典的key
  40. PyObject *me_key;
  41. //这个字段只对combined table有意义
  42. /*
  43. 还记得ma_values吗?上面说了如果是combined table,那么key和value都会存在PyDictKeysObject *ma_keys里面,但如果是split table,那就只有key会存在PyDictKeysObject *ma_keys里面,也就是这里me_key,所以这里注释了:me_value这个字段只对combined table有意义。因为是split table的话,value都会存储在ma_values里面,而不是这里的me_value
  44. */
  45. PyObject *me_value; /* This field is only meaningful for combined tables */
  46. } PyDictKeyEntry;

因此可以看到字典的定义还是蛮复杂的,但是仔细分析还是可以看懂的。PyDictObject里面有一个ma_values,如果是combined table,那么这个值是为NULL,key和value是放在PyDictKeyEntry里面的,由me_key和me_value存储,这当然也是一个PyObject *指针类型。如果是split table,那么ma_values则是一个数组,存储所有value,当然这里的value也是指针,PyDictKeyEntry则只存储key。而哈希表还要对应一个索引啊,这个索引都是放在PyDictKeysObject里面的。

6.2.1 再谈哈希表

从6.1中,我们知道了哈希表的基本思想,就是通过某个函数将需要搜索的键值映射为一个索引,然后通过索引去访问连续的内存区域。而对于哈希表这种数据结构,最终目的就是加速键的搜索过程。而用于映射的函数就是哈希函数,映射之后的值就是哈希值。因此在哈希表的实现中,哈希函数的优劣将直接决定实现的哈希表的搜索效率的高低。

并且我们知道,当元素到达容量的三分之二的时候,会很容易出现哈希值冲突,我们之前说如果冲突了,就改变规则重新映射。事实上,python也确实是这么做的,这种方法叫做开放寻址法。

当发生哈希值冲突时,python会通过一个二次探测函数f,计算下一个候选位置addr,如果可用就插入进去。如果不可用,会继续使用探测函数,直到找到一个可用的位置。

通过多次使用探测函数f,从一个位置可以到达多个位置,我们认为这些位置形成了一个"冲突探测链(探测序列)",比如当我们插入一个key="satori"的键值对,在a位置发现不行,又走b位置,发现也被人占了,于是到达c位置,发现没有key,于是就占了c这个位置。但是问题来了,如果我此时把b位置上键值对给删掉会引发什么后果?首先我们知道,b位置上的key和我们指定的值为"satori"的key通过哈希函数映射出来的索引是一样的,当我们直接获取d["satori"],肯定会先走a位置,发现有人但key又不是"satori",于是重新映射,走到b,发现还不对,再走到c位置,发现key是"satori",于是就把值取出来了。但是,我要说但是了,如果我们把b位置上的元素删掉呢?那么老规矩,获取、映射、走到a发现坑被占、走到b结果发现居然没有内容,那么直接就报出了一个KeyError。继续寻找的前提是,这个地方要存储了key、value,并且存在的key和指定的key不相同,但如果没有的话,就说明根本没有这个key。然而呢?"satori"这个key确实是存在的,因此发生这种情况我们就说探测链断裂。本来应该走到c的,但是由于b没有元素,因此探测函数在b处就停止了

因此我们发现,当一个元素只要位于任何一条探测链当中,在删除元素时都不能真正意义上的删除,而是一种"伪删除"操作

6.2.2 entry的三种状态

还记得这个entry吗?对于字典里面的一个键值对就叫做一个entry

  1. typedef struct {
  2. Py_hash_t me_hash;
  3. PyObject *me_key;
  4. PyObject *me_value;
  5. } PyDictKeyEntry;

在python中,当一个PyDictObject对象发生变化时,其中的entry会在三种不同的状态之间进行切换:unused态、active态、dummy态。

  • 当一个entry的me_key和me_value都是NULL的时候,entry处于unused态。unused态表明该entry中并没有存储key、value,并且在此之前也没有存储过它们。每一个entry在初始化的时候都会处于这个状态,me_value不管何时都可能会NULL,这取决于到底是combined table、还是split table,但是对于me_key,只可能在unused的时候才可能会NULL。
  • 当entry存储了key时,那么此时entry便从unused态变成了active态
  • 当entry中的key(value)被删除后,状态便从active态变成dummy态,注意:这里是dummy,删除了并不代表就能够回到unused态,来存储其他key了。我们也说了,unused态是指当前没有、并且之前也没有存储过。key被删除后,会变成dummy。否则就会发生我们之前说的探测链断裂,至于这个dummy到底是啥,我们后面说。总是entry进入dummy态,就是我们刚才提到的伪删除技术,当python沿着某条探测链搜索时,如果发现一个entry处于dummy态,就会明白虽然当前的entry是无效的,但是后面的entry可能是有效的,而不会直接就停止搜索、报错,这样就保证了探测链的连续性。至于报错,是在找到了unused状态的entry时才会报错,因为这里确实一直都没有存储过key,但是索引确实是这个位置,这说明当前指定的key就真的不存在哈希表中,此时才会报错。

6.3 PyDictObject的创建与维护

6.3.1 PyDictObject的创建

python内部通过PyDict_New来创建一个新的dict对象。

  1. PyObject *
  2. PyDict_New(void)
  3. {
  4. //new_keys_object表示创建PyDictKeysObject*对象
  5. //里面传一个数值,表示entry的容量
  6. //#define PyDict_MINSIZE 8,从宏定义我们能看出来为8
  7. //表示默认初始化能容纳8个entry的PyDictKeysObject
  8. //为什么是8,这是通过大量的经验得来的。
  9. PyDictKeysObject *keys = new_keys_object(PyDict_MINSIZE);
  10. if (keys == NULL)
  11. return NULL;
  12. //这一步则是根据PyDictKeysObject *创建一个新字典
  13. return new_dict(keys, NULL);
  14. }
  15. static PyDictKeysObject *new_keys_object(Py_ssize_t size)
  16. {
  17. PyDictKeysObject *dk;
  18. Py_ssize_t es, usable;
  19. //检测,size是否>=PyDict_MINSIZE
  20. assert(size >= PyDict_MINSIZE);
  21. assert(IS_POWER_OF_2(size));
  22. usable = USABLE_FRACTION(size);
  23. //es:哈希表中的每个索引占多少字节
  24. if (size <= 0xff) {
  25. es = 1;
  26. }
  27. else if (size <= 0xffff) {
  28. es = 2;
  29. }
  30. #if SIZEOF_VOID_P > 4
  31. else if (size <= 0xffffffff) {
  32. es = 4;
  33. }
  34. #endif
  35. else {
  36. es = sizeof(Py_ssize_t);
  37. }
  38. //注意到,字典里面也有缓冲池,当然这里指定是字典的key
  39. //如果有的话,直接从里面取
  40. if (size == PyDict_MINSIZE && numfreekeys > 0) {
  41. dk = keys_free_list[--numfreekeys];
  42. }
  43. else {
  44. //否则malloc重新申请
  45. dk = PyObject_MALLOC(sizeof(PyDictKeysObject)
  46. + es * size
  47. + sizeof(PyDictKeyEntry) * usable);
  48. if (dk == NULL) {
  49. PyErr_NoMemory();
  50. return NULL;
  51. }
  52. }
  53. //设置引用计数、可用的entry个数等信息
  54. DK_DEBUG_INCREF dk->dk_refcnt = 1;
  55. dk->dk_size = size;
  56. dk->dk_usable = usable;
  57. //dk_lookup很关键,里面包括了哈希函数和冲突时的二次探测函数的实现
  58. dk->dk_lookup = lookdict_unicode_nodummy;
  59. dk->dk_nentries = 0;
  60. //哈希表的初始化
  61. memset(&dk->dk_indices[0], 0xff, es * size);
  62. memset(DK_ENTRIES(dk), 0, sizeof(PyDictKeyEntry) * usable);
  63. return dk;
  64. /*
  65. keys.entries和values按照顺序
  66. */
  67. }
  68. static PyObject *
  69. new_dict(PyDictKeysObject *keys, PyObject **values)
  70. {
  71. PyDictObject *mp;
  72. assert(keys != NULL);
  73. //这是一个字典的缓冲池
  74. if (numfree) {
  75. mp = free_list[--numfree];
  76. assert (mp != NULL);
  77. assert (Py_TYPE(mp) == &PyDict_Type);
  78. _Py_NewReference((PyObject *)mp);
  79. }
  80. //系统堆中申请内存
  81. else {
  82. mp = PyObject_GC_New(PyDictObject, &PyDict_Type);
  83. if (mp == NULL) {
  84. DK_DECREF(keys);
  85. free_values(values);
  86. return NULL;
  87. }
  88. }
  89. //设置key、value等等
  90. mp->ma_keys = keys;
  91. mp->ma_values = values;
  92. mp->ma_used = 0;
  93. mp->ma_version_tag = DICT_NEXT_VERSION();
  94. assert(_PyDict_CheckConsistency(mp));
  95. return (PyObject *)mp;
  96. }

6.3.2 PyDictObject的元素搜索

python为哈希表搜索提供了多种函数,lookdict、lookdict_unicode、lookdict_index,一般通用的是lookdict,lookdict_unicode则是专门针对key为unicode的entry,lookdict_index针对key为int的entry,可以把lookdict_unicode、lookdict_index看成lookdict的特殊实现,只不过这两种可以非常的常用,因此单独实现了一下。

注意:我们无论是对字典设置值还是获取值,都需要进行搜索策略。我们来看看lookdict的底层实现

  1. static Py_ssize_t _Py_HOT_FUNCTION
  2. lookdict(PyDictObject *mp, PyObject *key,
  3. Py_hash_t hash, PyObject **value_addr)
  4. {
  5. size_t i, mask, perturb;
  6. //keys数组的首地址
  7. PyDictKeysObject *dk;
  8. //entries数组的首地址
  9. PyDictKeyEntry *ep0;
  10. top:
  11. dk = mp->ma_keys;
  12. ep0 = DK_ENTRIES(dk);
  13. mask = DK_MASK(dk);
  14. perturb = hash;
  15. //哈希,定位探测链冲突的第一个entry的索引
  16. i = (size_t)hash & mask;
  17. for (;;) {
  18. // dk->indecs[i]
  19. Py_ssize_t ix = dk_get_index(dk, i);
  20. //如果ix == DKIX_EMPTY,说明没有存储值
  21. //理论上是报错的,但是在底层是将值的指针设置为NULL
  22. if (ix == DKIX_EMPTY) {
  23. *value_addr = NULL;
  24. return ix;
  25. }
  26. if (ix >= 0) {
  27. //拿到指定的entry的指针
  28. PyDictKeyEntry *ep = &ep0[ix];
  29. assert(ep->me_key != NULL);
  30. //如果两个key一样,那么直接将值的地址设置为ep->me_value
  31. /*
  32. 但是注意这里的一样,相当于在python中,两个地址一样的对象
  33. 也就是说,a is b是为True
  34. */
  35. if (ep->me_key == key) {
  36. *value_addr = ep->me_value;
  37. return ix;
  38. }
  39. //如果两个对象不一样,那么就比较它们的哈希值是否相同
  40. //比如33和33是一个对象,但是3333和3333却不是,但是它们的值是一样的
  41. //因此先判断id是否一致,如果不一致再比较值是否一样,当然这里是哈希值
  42. if (ep->me_hash == hash) {
  43. PyObject *startkey = ep->me_key;
  44. Py_INCREF(startkey);
  45. int cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
  46. Py_DECREF(startkey);
  47. if (cmp < 0) {
  48. *value_addr = NULL;
  49. return DKIX_ERROR;
  50. }
  51. if (dk == mp->ma_keys && ep->me_key == startkey) {
  52. if (cmp > 0) {
  53. *value_addr = ep->me_value;
  54. return ix;
  55. }
  56. }
  57. else {
  58. /* The dict was mutated, restart */
  59. goto top;
  60. }
  61. }
  62. }
  63. //如果条件均不满足,调整姿势,进行下一次探索
  64. perturb >>= PERTURB_SHIFT;
  65. i = (i*5 + perturb + 1) & mask;
  66. }
  67. Py_UNREACHABLE();
  68. }

6.3.4 插入元素

我们对PyDictObject对象的操作都是建立在搜索的基础之上的,插入和删除也不例外。

  1. static int
  2. insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
  3. {
  4. PyObject *old_value;
  5. PyDictKeyEntry *ep;
  6. //增加对key和value的引用计数
  7. Py_INCREF(key);
  8. Py_INCREF(value);
  9. //类型检查
  10. if (mp->ma_values != NULL && !PyUnicode_CheckExact(key)) {
  11. if (insertion_resize(mp) < 0)
  12. goto Fail;
  13. }
  14. Py_ssize_t ix = mp->ma_keys->dk_lookup(mp, key, hash, &old_value);
  15. if (ix == DKIX_ERROR)
  16. goto Fail;
  17. assert(PyUnicode_CheckExact(key) || mp->ma_keys->dk_lookup == lookdict);
  18. MAINTAIN_TRACKING(mp, key, value);
  19. /* 检查共享key,可能扩容哈希表
  20. */
  21. if (_PyDict_HasSplitTable(mp) &&
  22. ((ix >= 0 && old_value == NULL && mp->ma_used != ix) ||
  23. (ix == DKIX_EMPTY && mp->ma_used != mp->ma_keys->dk_nentries))) {
  24. if (insertion_resize(mp) < 0)
  25. goto Fail;
  26. ix = DKIX_EMPTY;
  27. }
  28. //搜索成功
  29. if (ix == DKIX_EMPTY) {
  30. /* 插入一个新的slot,这个slot可以直接看成是entry */
  31. assert(old_value == NULL);
  32. if (mp->ma_keys->dk_usable <= 0) {
  33. /* 需要resize */
  34. if (insertion_resize(mp) < 0)
  35. goto Fail;
  36. }
  37. //寻找值的插入位置,就是我们之前说的将key这个值通过哈希函数映射为索引
  38. Py_ssize_t hashpos = find_empty_slot(mp->ma_keys, hash);
  39. //拿到PyDictKeyEntry *指针
  40. ep = &DK_ENTRIES(mp->ma_keys)[mp->ma_keys->dk_nentries];
  41. //设置
  42. dk_set_index(mp->ma_keys, hashpos, mp->ma_keys->dk_nentries);
  43. ep->me_key = key; //设置key
  44. ep->me_hash = hash;//设置哈希
  45. //如果ma_values数组不为空
  46. if (mp->ma_values) {
  47. assert (mp->ma_values[mp->ma_keys->dk_nentries] == NULL);
  48. //设置进去,还记得这是什么表吗?对,这是一张split table
  49. mp->ma_values[mp->ma_keys->dk_nentries] = value;
  50. }
  51. else {
  52. //ma_values数据为空的话,那么value就设置在PyDictKeyEntry对象的me_value里面
  53. ep->me_value = value;
  54. }
  55. mp->ma_used++;//使用个数+1
  56. mp->ma_version_tag = DICT_NEXT_VERSION();//版本数+1
  57. mp->ma_keys->dk_usable--;//可用数-1
  58. mp->ma_keys->dk_nentries++;//里面entry数量+1
  59. assert(mp->ma_keys->dk_usable >= 0);
  60. assert(_PyDict_CheckConsistency(mp));
  61. return 0;
  62. }
  63. //判断key是否存在,存在即替换
  64. if (_PyDict_HasSplitTable(mp)) {
  65. mp->ma_values[ix] = value;
  66. if (old_value == NULL) {
  67. /* pending state */
  68. assert(ix == mp->ma_used);
  69. mp->ma_used++;
  70. }
  71. }
  72. else {
  73. assert(old_value != NULL);
  74. DK_ENTRIES(mp->ma_keys)[ix].me_value = value;
  75. }
  76. mp->ma_version_tag = DICT_NEXT_VERSION();
  77. Py_XDECREF(old_value); /* which **CAN** re-enter (see issue #22653) */
  78. assert(_PyDict_CheckConsistency(mp));
  79. Py_DECREF(key);
  80. return 0;
  81. Fail:
  82. Py_DECREF(value);
  83. Py_DECREF(key);
  84. return -1;
  85. }

以上是插入元素,我们看到无论是插入元素、还是设置元素,insertdict都是可以胜任。但是请注意一下参数,有一个hash参数,这个hash是从什么地方获取的呢?答案是,在调用这个insertdict之前其实会首先调用PyDict_SetItem

  1. int
  2. PyDict_SetItem(PyObject *op, PyObject *key, PyObject *value)
  3. {
  4. PyDictObject *mp;
  5. Py_hash_t hash;
  6. if (!PyDict_Check(op)) {
  7. PyErr_BadInternalCall();
  8. return -1;
  9. }
  10. assert(key);
  11. assert(value);
  12. mp = (PyDictObject *)op;
  13. //计算hash值
  14. if (!PyUnicode_CheckExact(key) ||
  15. (hash = ((PyASCIIObject *) key)->hash) == -1)
  16. {
  17. //
  18. hash = PyObject_Hash(key);
  19. if (hash == -1)
  20. return -1;
  21. }
  22. /* 调用insertdict,必要时调整元素 */
  23. return insertdict(mp, key, hash, value);
  24. }

我们说如果entry个数达到容量的三分之二,那么会调整容量,如何调整呢?

  1. //增长率
  2. #define GROWTH_RATE(d) ((d)->ma_used*3)
  3. static int
  4. insertion_resize(PyDictObject *mp)
  5. {
  6. //本质上调用了dictresize,传入PyDictObject * 和增长率
  7. return dictresize(mp, GROWTH_RATE(mp));
  8. }
  9. static int
  10. dictresize(PyDictObject *mp, Py_ssize_t minsize)
  11. {
  12. //新的容量,entry的个数
  13. Py_ssize_t newsize, numentries;
  14. //老的keys
  15. PyDictKeysObject *oldkeys;
  16. //老的values
  17. PyObject **oldvalues;
  18. //老的entries,新的entries
  19. PyDictKeyEntry *oldentries, *newentries;
  20. /* 确定table的大小*/
  21. for (newsize = PyDict_MINSIZE;
  22. newsize < minsize && newsize > 0;
  23. newsize <<= 1)
  24. ;
  25. if (newsize <= 0) {
  26. PyErr_NoMemory();
  27. return -1;
  28. }
  29. //获取原来的所有keys
  30. oldkeys = mp->ma_keys;
  31. /* 创建能够容纳newsize个entry的内存空间 */
  32. mp->ma_keys = new_keys_object(newsize);
  33. if (mp->ma_keys == NULL) {
  34. //把以前的key拷贝过去。
  35. /*
  36. 扩容并不是在本地扩容的,我们知道python存储的都是指针
  37. 当扩容之后,会在另一个地方申请更大的内存,然后会把之前的内容都拷贝过去
  38. 还是那句话,存储的是指针,不管拷贝到什么地方去,指针是不会变的,当然指针指向的值也是不会变的
  39. 但是指针的地址会变,因为指针也是一个变量,存储的是指针, 所以叫做指针变量
  40. 但不管咋样,总归是变量,自然也是有地址的,指针的指针就是我们所说的二级指针
  41. 可以承认的是, 拷贝之后,这些二级指针肯定会变。
  42. 然而在python中是体现不出来的,因为python里面没有二级指针的概念,甚至指针也没有。
  43. 你只能通过id查看内存地址,比如列表,虽然列表里面存储的本身就是地址,但是获取的时候确实个指针指向的值。
  44. 当然使用id查看地址,其实查看的就是列表里面的指针指向的值的地址,对,说白了就是列表里面的元素(指针)本身。
  45. 因此地址的地址你在python中是看不到的。
  46. */
  47. mp->ma_keys = oldkeys;
  48. return -1;
  49. }
  50. //必须满足 可用 >= 已用
  51. assert(mp->ma_keys->dk_usable >= mp->ma_used);
  52. if (oldkeys->dk_lookup == lookdict)
  53. mp->ma_keys->dk_lookup = lookdict;
  54. //获取已用entries
  55. numentries = mp->ma_used;
  56. //获取旧信息
  57. oldentries = DK_ENTRIES(oldkeys);
  58. newentries = DK_ENTRIES(mp->ma_keys);
  59. oldvalues = mp->ma_values;
  60. //如果oldvalues不为NULL,这应该是一个combined table
  61. //split table的特点是key是能是unicode、
  62. //那么需要把split table转换成combined table
  63. if (oldvalues != NULL) {
  64. for (Py_ssize_t i = 0; i < numentries; i++) {
  65. assert(oldvalues[i] != NULL);
  66. //将ma_values数组里面的元素统统都设置到PyDictKeyEntry对象里面去
  67. PyDictKeyEntry *ep = &oldentries[i];
  68. PyObject *key = ep->me_key;
  69. Py_INCREF(key);
  70. newentries[i].me_key = key;
  71. newentries[i].me_hash = ep->me_hash;
  72. newentries[i].me_value = oldvalues[i];
  73. }
  74. //减少原来对oldkeys的引用计数
  75. DK_DECREF(oldkeys);
  76. //将ma_values设置为NULL,因为所有的value都存在了PyDictKeyEntry对象的me_value里面
  77. mp->ma_values = NULL;
  78. if (oldvalues != empty_values) {
  79. free_values(oldvalues);
  80. }
  81. }
  82. else { // 否则的话说明这本身就是一个combined table
  83. if (oldkeys->dk_nentries == numentries) {
  84. //将就得entries拷贝到新的entries里面去
  85. memcpy(newentries, oldentries, numentries * sizeof(PyDictKeyEntry));
  86. }
  87. else {
  88. //处理旧的entries
  89. //active态的entry搬到新table中
  90. //dummy态的entry,调整key的引用计数,丢弃该entry
  91. PyDictKeyEntry *ep = oldentries;
  92. for (Py_ssize_t i = 0; i < numentries; i++) {
  93. while (ep->me_value == NULL)
  94. ep++;
  95. newentries[i] = *ep++;
  96. }
  97. }
  98. //字典缓冲池的操作,后面介绍
  99. assert(oldkeys->dk_lookup != lookdict_split);
  100. assert(oldkeys->dk_refcnt == 1);
  101. if (oldkeys->dk_size == PyDict_MINSIZE &&
  102. numfreekeys < PyDict_MAXFREELIST) {
  103. DK_DEBUG_DECREF keys_free_list[numfreekeys++] = oldkeys;
  104. }
  105. else {
  106. DK_DEBUG_DECREF PyObject_FREE(oldkeys);
  107. }
  108. }
  109. //建立哈希表索引
  110. build_indices(mp->ma_keys, newentries, numentries);
  111. mp->ma_keys->dk_usable -= numentries;
  112. mp->ma_keys->dk_nentries = numentries;
  113. return 0;
  114. }

我们再来看一下改变dict内存空间的一些动作

  • 首先要确定table的大小,很显然这个大小一定要大于minsize,这个minsize通过我们已经看到了,是通过宏定义的,是已用entry的3倍
  • 根据新的table,重新申请内存
  • 将原来的处于active状态的entry拷贝到新的内存当中,而对于处于dummy状态的entry则直接丢弃。之所以可以丢弃,是因为,dummy状态的entry存在是为了保证探测链不断裂,但是现在所有的active都拷贝到新的内存当中了,它们会形成一条新的探测链,因此也就不需要这些dummy态的entry了
  • 建立的新的索引,并且如果之前的table指向了一片系统堆的内存空间,那么我们还需要释放,以防止内存泄漏。

6.3.5 删除元素

插入元素(设置元素)如果明白了,删除元素我觉得都可以不需要说了。

  1. int
  2. PyDict_DelItem(PyObject *op, PyObject *key)
  3. {
  4. //这显然和dictresize一样,是先获取hash值
  5. Py_hash_t hash;
  6. assert(key);
  7. if (!PyUnicode_CheckExact(key) ||
  8. (hash = ((PyASCIIObject *) key)->hash) == -1) {
  9. hash = PyObject_Hash(key);
  10. if (hash == -1)
  11. return -1;
  12. }
  13. //真正来删除是下面这个函数
  14. return _PyDict_DelItem_KnownHash(op, key, hash);
  15. }
  16. int
  17. _PyDict_DelItem_KnownHash(PyObject *op, PyObject *key, Py_hash_t hash)
  18. {
  19. Py_ssize_t ix;
  20. PyDictObject *mp;
  21. PyObject *old_value;
  22. //类型检测
  23. if (!PyDict_Check(op)) {
  24. PyErr_BadInternalCall();
  25. return -1;
  26. }
  27. assert(key);
  28. assert(hash != -1);
  29. mp = (PyDictObject *)op;
  30. //获取对应entry的index
  31. ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &old_value);
  32. if (ix == DKIX_ERROR)
  33. return -1;
  34. if (ix == DKIX_EMPTY || old_value == NULL) {
  35. _PyErr_SetKeyError(key);
  36. return -1;
  37. }
  38. // split table不支持删除操作,如果是split table,需要转换成combined table
  39. if (_PyDict_HasSplitTable(mp)) {
  40. if (dictresize(mp, DK_SIZE(mp->ma_keys))) {
  41. return -1;
  42. }
  43. ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &old_value);
  44. assert(ix >= 0);
  45. }
  46. //传入hash和ix,又调用了delitem_common
  47. return delitem_common(mp, hash, ix, old_value);
  48. }
  49. static int
  50. delitem_common(PyDictObject *mp, Py_hash_t hash, Py_ssize_t ix,
  51. PyObject *old_value)
  52. {
  53. PyObject *old_key;
  54. PyDictKeyEntry *ep;
  55. //找到对应的hash索引
  56. Py_ssize_t hashpos = lookdict_index(mp->ma_keys, hash, ix);
  57. assert(hashpos >= 0);
  58. //已经entries个数-1
  59. mp->ma_used--;
  60. //版本-1
  61. mp->ma_version_tag = DICT_NEXT_VERSION();
  62. //拿到entry的指针
  63. ep = &DK_ENTRIES(mp->ma_keys)[ix];
  64. //将其设置为dummy状态
  65. dk_set_index(mp->ma_keys, hashpos, DKIX_DUMMY);
  66. ENSURE_ALLOWS_DELETIONS(mp);
  67. old_key = ep->me_key;
  68. //将其key、value都设置为NULL
  69. ep->me_key = NULL;
  70. ep->me_value = NULL;
  71. //减少引用计数
  72. Py_DECREF(old_key);
  73. Py_DECREF(old_value);
  74. assert(_PyDict_CheckConsistency(mp));
  75. return 0;
  76. }

流程非常清晰,也很简单。先使用PyDict_DelItem计算hash值,再使用_PyDict_DelItem_KnownHash计算出索引,最后使用delitem_common获取相应的entry,删除维护的元素,并将entry从active态设置为dummy态,同时还会调整ma_used(已用entry)的数量

6.4 PyDictObject对象缓冲池

从介绍PyLongObject的小整数对象池的时候,我们就说过,不同的对象都有自己的缓冲池,比如list,当然dict也不例外。

  1. #ifndef PyDict_MAXFREELIST
  2. #define PyDict_MAXFREELIST 80
  3. #endif
  4. static PyDictObject *free_list[PyDict_MAXFREELIST];
  5. static int numfree = 0;

PyDictObject的缓冲池机制其实和PyListObject的缓冲池是类似的,开始时,这个缓冲池什么也没有,直到第一个PyDictObject对象被销毁时,这个PyDictObject缓冲池里面才开始接纳被缓冲的PyDictObject对象。

  1. static void
  2. dict_dealloc(PyDictObject *mp)
  3. {
  4. //获取ma_values指针
  5. PyObject **values = mp->ma_values;
  6. //获取所有的ma_keys指针
  7. PyDictKeysObject *keys = mp->ma_keys;
  8. //两个整型
  9. Py_ssize_t i, n;
  10. //追踪、调试
  11. PyObject_GC_UnTrack(mp);
  12. Py_TRASHCAN_SAFE_BEGIN(mp)
  13. //调整引用计数
  14. if (values != NULL) {
  15. if (values != empty_values) {
  16. for (i = 0, n = mp->ma_keys->dk_nentries; i < n; i++) {
  17. Py_XDECREF(values[i]);
  18. }
  19. free_values(values);
  20. }
  21. DK_DECREF(keys);
  22. }
  23. else if (keys != NULL) {
  24. assert(keys->dk_refcnt == 1);
  25. DK_DECREF(keys);
  26. }
  27. //将被销毁的对象放到缓冲池当中
  28. if (numfree < PyDict_MAXFREELIST && Py_TYPE(mp) == &PyDict_Type)
  29. free_list[numfree++] = mp;
  30. else
  31. Py_TYPE(mp)->tp_free((PyObject *)mp);
  32. Py_TRASHCAN_SAFE_END(mp)
  33. }

和PyListObject对象的缓冲池机制一样,缓冲池中只保留了PyDictObject对象。如果维护的维护的是从系统堆中申请的内存空间,那么python将释放这份内存空间,归还给系统堆。如果不是,那么仅仅只需要调整维护的对象的引用计数即可

其实在创建一个PyDictObject对象时,如果缓冲池中有可用的对象,也会直接从缓冲池中取,而不需要再重新创建。

  1. static PyObject *
  2. new_dict(PyDictKeysObject *keys, PyObject **values)
  3. {
  4. PyDictObject *mp;
  5. assert(keys != NULL);
  6. if (numfree) {
  7. mp = free_list[--numfree];
  8. assert (mp != NULL);
  9. assert (Py_TYPE(mp) == &PyDict_Type);
  10. _Py_NewReference((PyObject *)mp);
  11. }
  12. ...
  13. ...
  14. ...

《python解释器源码剖析》第6章--python中的dict对象的更多相关文章

  1. 《python解释器源码剖析》第13章--python虚拟机中的类机制

    13.0 序 这一章我们就来看看python中类是怎么实现的,我们知道C不是一个面向对象语言,而python却是一个面向对象的语言,那么在python的底层,是如何使用C来支持python实现面向对象 ...

  2. 《python解释器源码剖析》第12章--python虚拟机中的函数机制

    12.0 序 函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作.当然在调用函数时,会干什么来着.对,要在运行时栈中创建栈帧,用于函数的执行. 在python ...

  3. 《python解释器源码剖析》第9章--python虚拟机框架

    9.0 序 下面我们就来剖析python运行字节码的原理,我们知道python虚拟机是python的核心,在源代码被编译成字节码序列之后,就将有python的虚拟机接手整个工作.python虚拟机会从 ...

  4. 《python解释器源码剖析》第0章--python的架构与编译python

    本系列是以陈儒先生的<python源码剖析>为学习素材,所记录的学习内容.不同的是陈儒先生的<python源码剖析>所剖析的是python2.5,本系列对应的是python3. ...

  5. 《python解释器源码剖析》第1章--python对象初探

    1.0 序 对象是python中最核心的一个概念,在python的世界中,一切都是对象,整数.字符串.甚至类型.整数类型.字符串类型,都是对象.换句话说,python中面向对象的理念观测的非常彻底,面 ...

  6. 《python解释器源码剖析》第11章--python虚拟机中的控制流

    11.0 序 在上一章中,我们剖析了python虚拟机中的一般表达式的实现.在剖析一遍表达式是我们的流程都是从上往下顺序执行的,在执行的过程中没有任何变化.但是显然这是不够的,因为怎么能没有流程控制呢 ...

  7. 《python解释器源码剖析》第8章--python的字节码与pyc文件

    8.0 序 我们日常会写各种各样的python脚本,在运行的时候只需要输入python xxx.py程序就执行了.那么问题就来了,一个py文件是如何被python变成一系列的机器指令并执行的呢? 8. ...

  8. 《python解释器源码剖析》第7章--python中的set对象

    7.0 序 集合和字典一样,都是性能非常高效的数据结构,性能高效的原因就在于底层使用了哈希表.因此集合和字典的原理本质上是一样的,都是把值映射成索引,通过索引去查找. 7.1 PySetObject ...

  9. 《python解释器源码剖析》第4章--python中的list对象

    4.0 序 python中的list对象,底层对应的则是PyListObject.如果你熟悉C++,那么会很容易和C++中的list联系起来.但实际上,这个C++中的list大相径庭,反而和STL中的 ...

  10. 《python解释器源码剖析》第2章--python中的int对象

    2.0 序 在所有的python内建对象中,整数对象是最简单的对象.从对python对象机制的剖析来看,整数对象是一个非常好的切入点.那么下面就开始剖析整数对象的实现机制 2.1 初识PyLongOb ...

随机推荐

  1. RDD的cache 与 checkpoint 的区别

    问题:cache 与 checkpoint 的区别? 关于这个问题,Tathagata Das 有一段回答: There is a significant difference between cac ...

  2. HDFS文件目录操作代码

    分布式文件系统HDFS中对文件/目录的相关操作代码,整理了一下,大概包括以下部分: 文件夹的新建.删除.重命名 文件夹中子文件和目录的统计 文件的新建及显示文件内容 文件在local和remote间的 ...

  3. 【神经网络与深度学习】转-caffe安装吐血总结

    这周安装了caffe的windows版本和Linux版本,依赖关系太多,如果系统选对了,安装起来很easy,选错了,就会遇见各种坑. 1.操作系统最好使用ubuntu desktop 14.04 64 ...

  4. 看kubelet的日志 + Kubeadm安装Kubernetes环境

    1.通过journalctl看日志 journalctl -xeu kubelet > a参考:https://www.cnblogs.com/ericnie/p/7749588.html

  5. gzip压缩配置

    gzip on;gzip_buffers 32 4K;gzip_comp_level 6;gzip_min_length 200;gzip_types text/css text/xml applic ...

  6. [Nowcoder212D]禁书目录_概率期望

    禁书目录 题目大意:清教需要定期给Index清除记忆,在此之前需要把当中的十万三千本禁书取出来......不幸的是,禁书一旦离开了Index就非常脆弱,具体来说,每一本禁书都有一个魔力值 ai ,其记 ...

  7. SQL SERVER 中的smalldatetime和datetime区别

    原文:SQL SERVER 中的smalldatetime和datetime区别 smalldatetime不能到秒. 不過它占的空間小.(4位) datetime(8位) 而且兩者的時間範圍不一樣. ...

  8. 【LOJ】#3046. 「ZJOI2019」语言

    LOJ#3046. 「ZJOI2019」语言 先orz zsy吧 有一个\(n\log^3n\)的做法是把树链剖分后,形成logn个区间,这些区间两两搭配可以获得一个矩形,求矩形面积并 然后就是对于一 ...

  9. PAT B1022 D进制的A+B

    课本AC代码 #include <cstdio> int main() { int a, b, d; scanf("%d%d%d", &a, &b, & ...

  10. 洛谷P1088 火星人

    //其实就是全排列 //我们从外星人给的那串数字往下搜索 //一直往下拓展m次 //最后输出结果 //虽然看起来很暴力,但是题目上说了m非常小 #include<bits/stdc++.h> ...