在迭代期间修改dict

时间:2016-06-06 21:38:10

标签: python dictionary

下面发生了什么

>>> d = {0:0}
>>> for i in d:
...     del d[i]
...     d[i+1] = 0
...     print(i)
...     
0
1
2
3
4
5
6
7
>>> 

为什么迭代停止在8而没有任何错误?

可在python2.7和python 3.5上重现。

2 个答案:

答案 0 :(得分:8)

dict中键表的初始大小为8个元素。所以0 ... 7设置第1到第8个元素,8设置第1个元素,结束循环。

Source: Objects/dictobject.c

  

/ * PyDict_MINSIZE_COMBINED是任何新的非拆分的起始大小   字典。 8允许dicts不超过5个活动条目;   实验表明,这足以满足大多数的要求   (主要包括为传递关键字而创建的通常小的dicts   参数)。使这个8而不是4减少了数量   为大多数词典调整大小,没有任何重要的额外记忆   使用。 * /

     

#define PyDict_MINSIZE_COMBINED 8

答案 1 :(得分:6)

此行为源自cpython static PyDictKeyEntry * lookdict(...)中的密钥查找算法,如document中所述:

  

所有操作使用的基本查找功能。这是基于   来自Knuth Vol的算法D. 3,Sec。 6.4。 ...初始探测指数   计算为哈希mod表格大小最初等于8 )。

在每个for循环的开头,内部调用dict_next函数来解析下一个元素的地址。该函数的核心是:

value_ptr = &mp->ma_keys->dk_entries[i].me_value;
mask = DK_MASK(mp->ma_keys); // size of the array which stores the key values (ma_keys)
while (i <= mask && *value_ptr == NULL) { // skip NULL elements ahead 
    value_ptr = (PyObject **)(((char *)value_ptr) + offset);
    i++;
}
if (i > mask)
    return -1; // raise StopIteration 

其中i是实际存储值的C数组的索引。如上所述,密钥的初始索引是从hash(key)%table_size计算的。数组中的另一个元素都设置为NULL,因为dict在测试用例中只包含一个元素。

鉴于hash(i)==i if i是一个int,你的例子中dict的内存布局将是:

1st iter: [0,   NULL,NULL,NULL,NULL,NULL,NULL,NULL]; i=0
2nd iter: [NULL,1   ,NULL,NULL,NULL,NULL,NULL,NULL]; i=1
...
8th iter: [NULL,NULL,NULL,NULL,NULL,NULL,NULL,7   ]; i=7

更有趣的测试案例是:

def f(d):
  for i in d:
    del d[i]
    print hash(i)%8
    d[str(hash(i))]=0
f({0:0})       # outputs 0,1,6
f({'hello':0}) # outputs 5,7
f({'world':0}) # outputs 1

总之,这种循环的退出条件是

hash(new_key)%8<=hash(old_key)%8