Python 为较小的字典替换键有效,但为较大的字典引发 KeyError

Python replacing keys for smaller dict works but raises KeyError for larger dict

代码 1:

kv = {'a': 'A', 'b': 'B', 'c': 'C'}
d = {'a': 1, 'b': 2, 'c': 3}
for key in d.keys():
    d[kv[key]] = d.pop(key)

print(d)

输出:

{'A': 1, 'B': 2, 'C': 3}

上面的例子工作正常
但是,当我们增加字典中的元素数量时,
它引发 KeyError 如下例所示 代码 2:

kv = {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}
d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
for key in d.keys():
    d[kv[key]] = d.pop(key)
print(d)

输出:

Traceback (most recent call last):
  File "C:\Users\user\OneDrive\Documents\VSCode_Python\new.py", line 11, in <module>
    d[kv[key]] = d.pop(key)
KeyError: 'A'

最简单的答案可以在 Python 关于“字典视图对象”的文档中找到:https://docs.python.org/3/library/stdtypes.html#dict-views:

Iterating views while adding or deleting entries in the dictionary may raise a RuntimeError or fail to iterate over all entries.

它说可能无法遍历所有条目。在您的 dict 长度为 4 的示例中,似乎迭代 keys() 字典视图对象有效(即,如果您没有在循环内改变字典,其行为方式相同)前 3 个条目(就在您依次弹出每个条目之前)然后在到达键 'd' 之前到达新设置的键 'A'。换句话说,迭代此视图 确实失败了 以迭代长度为 4 的字典的所有条目。

关于为什么长度为 3 的字典没有失败的问题也许可以参考实现细节来回答,这些细节使我们能够理解语言内部是如何产生的对于这种 luck-of-the-draw 行为,但最终,答案是 Python 语言规范不要求您的代码失败 以查看任何长度的字典,它恰好不会因长度为 3.

dict 而失败

请参阅@kcsquared 的评论和其他人的回答,以深入了解 implementation-specific 有关 the/a 当前 Python 语言实现的任意故障点的详细信息。

发生这种情况的真正原因是 Python 中字典的实现。这个article描述的很好

本质上,Python计算键的哈希值并应用掩码找到存储值的槽。

对于一个小字典,你有 8 个槽位,所以确定你把值放在哪个槽位的函数是:

def find_slot(key):
    return hash(key) & 7

你会注意到原始字典中没有冲突:

list(map(find_slot, ('a', 'b', 'c', 'd')))
>>>[5, 0, 6, 1]

但是在添加'A'键的时候出现了冲突:

list(map(find_slot, ('a', 'b', 'c', 'd', 'A')))
>>>[5, 0, 6, 1, 5]

'a''A' 提供了相同的槽位,因此字典必须扩展以包含更多槽位。

在字典展开之前,键散列存储在缓存中。当它扩展时,必须更新缓存。您可以在一些打印语句中看到这种情况:

from sys import getsizeof

kv = {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}
d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
for key in d.keys():
    print(d)
    print(getsizeof(d))
    print(d.keys(), key)
    d[kv[key]] = d.pop(key)
print(d)

这就是原理。不确定细节是否 100% 正确,但如果有人有任何意见,我们很乐意修复它。