让我们考虑这个代码,它会迭代列表,同时在每次迭代时删除一个项目:
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
没有像字典一样被提出?这种行为有什么好的理由吗?
答案 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])
当然,有更好,更直观的方法来执行此操作,但关键是它有效。
相比之下,dict
和set
的行为完全是特定于实现的,因为迭代顺序可能会根据散列而改变。
您获得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)
,因为这将从字典的键中生成一个列表,该列表在迭代期间不会改变