科技 · 实用指南
Python 报“dictionary changed size during iteration”?复制或加锁前先证明谁改了字典
区分同一循环修改、并发写入、回调重入和运行时注册表,再选择可验收的语义修复。

RuntimeError: dictionary changed size during iteration 只说明遍历期间映射发生了结构变化,并不告诉你修改者是当前循环、其他线程、回调,还是 Python/依赖维护的注册表。
保全堆栈,证明映射所有者与修改路径,再选择快照、两阶段修改、共享同步或上游修复,并在原失败交错下验证数据不变量。
1. 保全堆栈并点名具体映射
保存完整堆栈、Python 与依赖版本、线程或任务名,以及映射的稳定标识。确认失败帧位于自己的循环、deepcopy、weakref、sys.modules、执行器还是依赖内部。
记录遍历的是 d、keys、values 还是 items,并区分新增/删除键与只更新既有值;更新值时调用的回调仍可能结构性改键。
2. 一次只复现一类修改者
先关闭回调与后台写者;若仍失败,检查循环体及其调用的 delete、pop、clear、缺键 setdefault 或插入。仅在负载下失败时,为所有写路径记录映射身份、操作、键哈希、线程/任务与时间。
不要因为堆栈落在 deepcopy 或 sorted 就认定它们发起了修改;弱引用回调、清理钩子、比较器、日志、信号或析构函数都可能在遍历时重入。
3. 有意识地选择快照或两阶段修改
若允许时间点视图,可按语义选择 list(d)、list(d.items())、tuple(d) 或 d.copy()。浅快照只稳定键遍历,不会复制可变值或保证后续复合读取一致。
若循环决定删改哪些键,先在不变视图上收集决定,再统一应用;若本意就是破坏性消费,明确的 popitem 循环更能表达语义。
4. 让并发与重入的所有权真正成立
多线程访问时,让需要一致性的读取和所有结构写入都经过同一个 Lock/RLock,或由单线程持有并通过队列通信。只创建锁或只锁读者不能约束未加锁写者。
回调场景不要持有任意锁调用未知代码;在所有权边界内取有界快照、释放后再回调,或把副作用排到第二阶段,并说明回调看到的是快照还是最新状态。
5. 把运行时注册表当依赖事故处理
堆栈落在 sys.modules、weakref、ProcessPoolExecutor、插件管理器或其他私有注册表时,先在受支持的新版本复现并查项目问题与变更记录,保留最小复现与修复版本。
验收要强制旧交错,核对键不丢不重、排序假设、回调只执行一次,并证明快照陈旧性是预期;仅不再抛错不算完成。
