Python如何检测字典迭代时的大小变更?该特性能否被编程覆盖?
Python遍历字典时修改大小的报错原因、检测机制及解决方案
问题复现
运行如下代码时会抛出异常:
my_dict = {1: 1} for key in my_dict: my_dict.pop(key)
报错信息为:
Traceback (most recent call last): File "<stdin>", line 1, in <module> RuntimeError: dictionary changed size during iteration
检测原理
CPython的字典对象内部维护了一个名为ma_version_tag的版本戳变量,每次对字典执行新增、删除、修改键值对这类会改动字典结构的操作时,这个版本戳就会自动加1。
当你启动对字典的遍历时,迭代器会在初始化瞬间记录当前字典的版本戳,之后每一次迭代取下一个元素前,都会校验当前字典的版本戳和初始记录的是否一致:一旦发现版本戳变化,就会直接抛出RuntimeError,避免因为字典结构变化导致迭代出现漏元素、重复遍历、访问无效内存等不可预期的问题。
你提到的5元素字典想遍历清空的逻辑之所以行不通,就是因为第一次pop操作就会把版本戳加1,第二次迭代校验不通过直接报错,根本走不到后面的pop步骤。
覆盖该限制的实现方式
完全可以实现遍历清空字典的需求,常用的有两种方案:
- 先快照键再遍历(推荐常规场景使用)
遍历前先把字典的所有键转成独立的列表,遍历这个列表而非原字典,就不会触发迭代校验逻辑:
my_dict = {1:1, 2:2, 3:3, 4:4, 5:5} # list(my_dict) 会一次性生成当前所有键的独立列表,和原字典迭代无关 for key in list(my_dict): my_dict.pop(key) # 执行完成后my_dict为空字典
- Hack底层版本戳(仅用于研究,不推荐任何生产场景使用)
如果就是想在直接遍历原字典的过程中修改大小,可以通过ctypes直接操作字典底层的版本戳,每次修改字典后把版本戳改回迭代器记录的初始值即可,不过这种实现强依赖CPython的底层实现,不同Python版本可能不兼容:
# 仅适用于CPython 3.10+ 版本,仅做演示 import ctypes class PyDictObject(ctypes.Structure): _fields_ = [ ("_ob_next", ctypes.c_void_p), ("_ob_prev", ctypes.c_void_p), ("ob_refcnt", ctypes.c_ssize_t), ("ob_type", ctypes.c_void_p), ("ma_used", ctypes.c_ssize_t), ("ma_version_tag", ctypes.c_uint64), # 后面的字段省略不影响我们修改版本戳 ] my_dict = {1:1, 2:2, 3:3} # 记录迭代前的版本戳 origin_version = PyDictObject.from_address(id(my_dict)).ma_version_tag for key in my_dict: my_dict.pop(key) # 改回原始版本戳,骗过迭代器校验 PyDictObject.from_address(id(my_dict)).ma_version_tag = origin_version # 执行完成后my_dict为空字典
如果只是需要清空字典,不需要遍历的话直接调用my_dict.clear()即可,性能更高。
内容的提问来源于stack exchange,提问作者michael
相关产品推荐
相关产品推荐

