在迭代期间修改列表和字典,为什么它在dict上失败?

时间:2018-04-04 08:47:53

标签: python list loops dictionary iteration

让我们考虑这个代码,它会迭代列表,同时在每次迭代时删除一个项目:

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没有像字典一样被提出?这种行为有什么好的理由吗?

3 个答案:

答案 0 :(得分:28)

我认为原因很简单。 list是有序的,dict s(在Python 3.6 / 3.7之前),set不是。因此,在迭代时修改list可能不会被视为最佳做法,但它会导致一致,可重现且有保证的行为。

您可以使用此功能,例如,假设您想要将偶数个元素的list分成两半并反转下半部分:

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

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

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

您获得RunTimeError collections.OrderedDict,大概是为了与dict行为保持一致。我不认为在Python 3.6(其中dict保证保持插入顺序)之后dict行为的任何变化都可能发生,因为它会破坏没有实际用例的向后兼容性。 / p>

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

答案 1 :(得分:7)

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

在较早的迭代器设计中,for循环通过调用具有增加的整数索引的序列元素检索钩子来工作,直到引发IndexError。 (我会说__getitem__,但这是在类型/类统一之前返回的,所以C类型没有__getitem__。)len甚至没有参与此设计,因此无处检查修改。

引入迭代器后,dict迭代器的大小更改检查从the very first commit that introduced iterators to the language开始。在此之前,所有Dict都是不可迭代的,因此没有向后兼容的问题。不过,列表仍然遵循旧的迭代协议。

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

答案 2 :(得分:2)

Dictionary使用带有附加级别间接的插入顺序,这会在删除和重新插入键时进行迭代时导致打嗝,从而更改字典的顺序和内部指针。

迭代d.keys()而不是d并未解决此问题,因为在Python 3中,d.keys()会返回dict中的键的动态视图在同样的问题。相反,迭代list(d),因为这将从字典的键中生成一个列表,该列表在迭代期间不会改变