迭代时修改list和dictionary,为啥在dict上会失败?

Posted

技术标签:

【中文标题】迭代时修改list和dictionary,为啥在dict上会失败?【英文标题】:Modify list and dictionary during iteration, why does it fail on dict?迭代时修改list和dictionary,为什么在dict上会失败? 【发布时间】:2018-09-13 18:06:21 【问题描述】:

让我们考虑一下这段代码,它在每次迭代中删除一个项目时迭代 list

x = list(range(5))

for i in x:
    print(i)
    x.pop()

它将打印0, 1, 2。由于列表中的最后两个元素在前两次迭代中被删除,因此只打印前三个元素。

但是如果你在 dict 上尝试类似的东西:

y = i: i for i in range(5)

for i in y:
    print(i)
    y.pop(i)

它将打印0,然后引发RuntimeError: dictionary changed size during iteration,因为我们在迭代时从字典中删除了一个键。

当然,在迭代期间修改列表是不好的。但是为什么 RuntimeError 不像字典那样被提出呢?这种行为有什么好的理由吗?

【问题讨论】:

如果可以实现列表,那肯定是救命稻草。 这也是我一直好奇的事情。该行为已记录在案,但在 docs.python.org/3/reference/compound_stmts.html#for 处没有得到证明。可能只是出于 *handwave* 性能原因?或者从一个角度来看是一种保守的选择——允许操作是因为它可以被允许,而在 3.6 之前的 dicts 上它是没有意义的,因为它们是无序的。 我可以想到在迭代列表时想要添加到列表的情况,有点像队列。 查看dict source code 对键进行迭代似乎并不简单,就像根本 这也可能与订单有关。如果您在迭代列表时附加到列表,则很清楚插入的项目的去向以及迭代的时间。标准字典没有顺序。 【参考方案1】:

我认为原因很简单。 lists 是有序的,dicts(在 Python 3.6/3.7 之前)和 sets 不是。因此,建议您在迭代时修改 lists 可能不是最佳实践,但它会导致一致、可重复且有保证的行为。

您可以使用它,例如,假设您想将具有偶数个元素的 list 分成两半并反转第二半:

>>> lst = [0,1,2,3]
>>> lst2 = [lst.pop() for _ in lst]
>>> lst, lst2
([0, 1], [3, 2])

当然,有更好、更直观的方法来执行此操作,但关键是它有效。

相比之下,dicts 和 sets 的行为完全是特定于实现的,因为迭代顺序可能会根据散列而改变。

你得到一个RunTimeErrorcollections.OrderedDict,大概是为了与dict 行为保持一致。我认为dict 行为在 Python 3.6 之后可能不会发生任何变化(dicts 保证保持插入顺序),因为它会破坏没有实际用例的向后兼容性。

请注意,collections.deque 在这种情况下也会引发 RuntimeError,尽管它已被订购。

【讨论】:

请注意,python 3.6 仍然存在异常 @Jean-FrançoisFabre 是的,我认为这不会改变,在我的回答中添加了更多想法【参考方案2】:

不可能在不破坏向后兼容性的情况下向列表添加这样的检查。对于dicts,没有这样的问题。

在旧的预迭代器设计中,for 循环通过调用具有递增整数索引的序列元素检索钩子来工作,直到它引发 IndexError。 (我会说__getitem__,但这是在类型/类统一之前,所以C类型没有__getitem__。)len甚至没有参与这个设计,也没有地方检查修改.

当引入迭代器时,dict 迭代器从the very first commit that introduced iterators to the language 开始检查大小变化。在此之前,字典根本不可迭代,因此没有向后兼容性可以打破。不过,列表仍然使用旧的迭代协议。

When list.__iter__ was introduced,这纯粹是一种速度优化,不是为了改变行为,添加修改检查会破坏与依赖旧行为的现有代码的向后兼容性。

【讨论】:

很有趣,我经常从您的回答中学到一些东西,但是您是否也同意我的回答,即结构的有序性质是否重要? @Chris_Rands:嗯,它确实使迭代期间的列表修改比 dicts 可能发生的更可预测,但这种行为仍然很奇怪而且不是很有用。使用其他不支持按索引进行随机访问的有序数据结构复制相同的行为也是低效或不可能的。我不认为排序是列表在迭代期间支持修改的令人信服的理由。 谢谢,我想接下来的问题是,为什么他们在引入 Python 3 时不趁机改变这一点(打破向后兼容性本来可以)? @Chris_Rands:我没有要引用的邮件列表讨论或任何东西,所以我只能提供一般性猜测:可能没有人推动它并完成这项工作,或者他们尝试过并认为性能损失不值得。【参考方案3】:

字典使用插入顺序和额外的间接级别,这会在删除和重新插入键时在迭代时导致打嗝,从而改变字典的顺序和内部指针。

这个问题并不能通过迭代 d.keys() 而不是 d 来解决,因为在 Python 3 中,d.keys() 返回 dict 中键的动态视图,这会导致同样的问题。相反,迭代list(d),因为这会从字典的键中生成一个列表,在迭代过程中不会改变

【讨论】:

以上是关于迭代时修改list和dictionary,为啥在dict上会失败?的主要内容,如果未能解决你的问题,请参考以下文章

如何遍历并删除Dictionary集合内容

如何遍历并删除Dictionary集合内容

为啥 Dictionary.First() 这么慢?

C# 使用定时器和Dictionary 出现异常说集合已修改,可能无法执行枚举操作,求帮忙啊

zip(), dict(), itertools.repeat(), list(迭代器)

Python学习:字典(dictionary)