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

duc*_*inh 29 python iteration dictionary loops list

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

x = list(range(5))

for i in x:
    print(i)
    x.pop()
Run Code Online (Sandbox Code Playgroud)

它会打印出来0, 1, 2.由于前两次迭代删除了列表中的最后两个元素,因此仅打印前三个元素.

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

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

for i in y:
    print(i)
    y.pop(i)
Run Code Online (Sandbox Code Playgroud)

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

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

Chr*_*nds 30

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

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

>>> lst = [0,1,2,3]
>>> lst2 = [lst.pop() for _ in lst]
>>> lst, lst2
([0, 1], [3, 2])
Run Code Online (Sandbox Code Playgroud)

当然,有更好,更直观的方法来执行此操作,但重点是它的工作原理.

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

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

请注意,尽管被命令,但在这种情况下collections.deque也提出了一个RuntimeError.


use*_*ica 8

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

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

当引入迭代器时,dict迭代器从将迭代器引入语言的第一笔提交中就进行了大小更改检查。在此之前,所有Dict都是不可迭代的,因此没有向后兼容的问题。不过,列表仍然通过了旧的迭代协议。

list.__iter__被引入,它是一个纯粹的速度优化,并非是一个行为改变,并添加修改检查将打破与上旧的行为依靠现有代码的向后兼容性。

  • @Chris_Rands:嗯,它的确可以使迭代过程中的列表修改比使用dict可以预测的要好得多,但是这种行为仍然很奇怪,并且不是很有用。用不支持按索引随机访问的其他有序数据结构来复制相同行为也是低效或不可能的。我不认为排序是列表在迭代过程中支持修改的令人信服的理由。 (2认同)