下面发生了什么
>>> 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上重现。
答案 0 :(得分:8)
dict中键表的初始大小为8个元素。所以0
... 7
设置第1到第8个元素,8设置第1个元素,结束循环。
/ * 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