DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Python 遍历字典删除空列表时报 dictionary changed size during iteration

Python 3 的字典视图会随原对象变化,遍历时删除空列表可能报 RuntimeError。按原地修改或新建结果选择键快照、两阶段删除或过滤,并核对误删和引用关系。

清理字典时,一边 for key in data,一边对空列表执行 pop(),可能在删掉部分条目后报 RuntimeError: dictionary changed size during iteration。把循环改成 for key in data.keys(),在 Python 3 中仍可能失败。

这里讨论单个执行流程清理普通 Python 3 字典的场景。若其他线程或回调也会修改字典,需要先处理访问协调;复制键列表不能自动保证整段业务操作的一致性。

keys 和 items 都是动态视图

典型代码如下:

data = {"ready": [1], "waiting": [], "done": [2, 3]}

for key in data:
    if not data[key]:
        data.pop(key)

循环正在读取字典的键,删除条目又改变了它正在遍历的结构。Python 3 的 keys()、values() 和 items() 返回动态视图,会反映字典变化,并不是创建时固定下来的副本。遍历期间增删条目可能报错,也可能不能完整遍历所有条目。

先查看循环体及它调用的函数,找出 pop()、del、clear() 或新增键的操作。不要只看 traceback 所在的 for 行。异常之前已经发生的删除不会自动撤销,重新运行前要确认当前数据是否已经变成部分清理后的状态。

要修改同一个字典,就遍历键快照

只需清理当前层级、且值保证都是列表时,可以写成:

data = {"ready": [1], "waiting": [], "done": [2, 3]}

for key in list(data):
    if not data[key]:
        del data[key]

print(data)  # 预期只剩 ready 和 done

list(data) 先保存这一时刻的键,循环随后遍历这个列表,删除原字典里的键不会改变列表本身。这里会额外存放一份键列表;不是对每个值做深拷贝。

如果字典中还允许 0、False、空字符串或 None,if not data[key] 也会把它们判为假。只想删除空列表时,应把条件收窄:

for key in list(data):
    value = data[key]
    if isinstance(value, list) and len(value) == 0:
        del data[key]

删除条件和遍历方式都要验证,避免消除了异常却多删数据。

需要先审核删除范围时,分两阶段处理

也可以先生成待删除键,再单独删除:

data = {"ready": [1], "waiting": [], "done": [2, 3]}

keys_to_remove = [
    key for key, value in data.items()
    if isinstance(value, list) and len(value) == 0
]

print(keys_to_remove)  # 先核对范围
for key in keys_to_remove:
    del data[key]

生成列表时不修改字典,删除时不遍历字典。这个结构便于在真正删除之前核对数量或记录键名。含敏感信息的键名不要直接写入公开日志。

如果审核和删除之间存在其他写入者,键可能已被删除,值也可能不再为空。简单改成 pop(key, None) 只会隐藏“键已不存在”,不能证明该键现在仍符合删除条件。此时应根据程序的并发模型重新设计临界区或任务归属。

新字典会改变引用关系

如果只需要一个过滤结果,可以保留原数据:

filtered = {
    key: value
    for key, value in data.items()
    if not (isinstance(value, list) and len(value) == 0)
}

其他变量若还指向原来的 data,不会自动变成指向 filtered。写成 data = filtered 也只是重新绑定这个名字;这与原地删除有不同的效果。过滤结果中的列表值仍引用原来的对象,之后修改列表内容会反映到共享该列表的地方。

不要把“复制外层键”推广为“嵌套结构都安全”。如果另一个内层循环也在删除内层字典的条目,需要在那个层级独立处理。

验证删除范围并保留恢复依据

先在与真实数据结构相同的小样本中包含空列表、非空列表,以及允许出现的其他值,检查应删和应保留的键。还要确认调用方是否要求保留原字典对象,以及过滤前后的值是否允许共享。

对只删除键、不会修改内部值的流程,可以在操作前保留字典的浅副本作为恢复依据。若原流程还修改了嵌套对象,浅副本不足以恢复这些修改。涉及文件或数据库持久化时,应在确认过滤结果后再提交;不要把捕获 RuntimeError 后继续运行当成清理完成。