Removing from a list while iterating over it
以下代码:
1 2 3 4 5 6 7 | a = list(range(10)) remove = False for b in a: if remove: a.remove(b) remove = not remove print(a) |
使用python 3.2时输出
请注意,我不是想围绕这种行为展开工作,而是想了解它。
我争论了一会儿,因为类似的问题在这里被问了很多次。但它的独特性足以让人从怀疑中获益。(尽管如此,如果其他人投票结束,我不会反对。)这里有一个关于正在发生的事情的直观解释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] <- b = 0; remove? no ^ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] <- b = 1; remove? yes ^ [0, 2, 3, 4, 5, 6, 7, 8, 9] <- b = 3; remove? no ^ [0, 2, 3, 4, 5, 6, 7, 8, 9] <- b = 4; remove? yes ^ [0, 2, 3, 5, 6, 7, 8, 9] <- b = 6; remove? no ^ [0, 2, 3, 5, 6, 7, 8, 9] <- b = 7; remove? yes ^ [0, 2, 3, 5, 6, 8, 9] <- b = 9; remove? no ^ |
既然没有其他人,我将尝试回答您的其他问题:
Why is no error given to indicate that underlying iterator is being modified?
为了在不禁止许多完全有效的循环构造的情况下抛出错误,python必须对正在发生的事情了解很多,并且可能必须在运行时获取这些信息。所有这些信息都需要时间来处理。这会使python慢得多,在速度真正重要的地方——一个循环。
Have the mechanics changed from earlier versions of Python with respect to this behaviour?
简言之,不,或者至少我高度怀疑它,而且自从我学习了Python(2.4)以来,它的行为方式当然是这样的。坦率地说,我希望可变序列的任何直接实现都能以这种方式工作。谁知道的更好,请纠正我。(实际上,一个快速的文档查找证实了Mikola引用的文本自1.4版起就一直在教程中!)
正如Mikola解释的那样,您观察到的实际结果是由于从列表中删除一个条目会将整个列表移动一个点,从而导致遗漏元素。
但在我看来,更有趣的问题是,当发生这种情况时,为什么Python不选择生成错误消息。如果您试图修改字典,它确实会产生这样的错误消息。我认为有两个原因。
dict在内部是复杂的,而列表则不是。列表基本上只是数组。一个dict必须在迭代时检测它的修改时间,以避免在dict的内部结构改变时崩溃。一个列表可以在不进行检查的情况下离开,因为它只是确保其当前索引仍在范围内。
历史上(我现在还不确定),python列表是通过使用[]操作符进行迭代的。python将对list[0]、list[1]、list[2]进行计算,直到得到indexerror。在这种情况下,python在开始之前没有跟踪列表的大小,因此它没有检测到列表大小已更改的方法。
当然,在迭代数组时修改它是不安全的。规范说这是个坏主意,行为未定义:
http://docs.python.org/tutorial/controlflow.html for语句
所以,下一个问题是,在这里,到底发生了什么?如果我不得不猜测的话,我会说它正在做这样的事情:
1 2 3 4 | for(int i=0; i<len(array); ++i) { do_loop_body(i); } |
如果您假设这确实是正在发生的事情,那么它将完全解释所观察到的行为。在当前指针处或指针之前删除元素时,将整个列表向左移动1。第一次,像往常一样删除1,但现在列表向后移动。下一个迭代不是命中2,而是命中3。然后删除一个4,列表向后移动。下一个迭代7,依此类推。
在你的第一次迭代中,你没有移除,所有的东西都很漂亮。
第二次迭代时,您处于序列的位置[1],然后删除"1"。然后迭代器将您带到序列中的位置[2],该位置现在为"3",因此跳过"2"(因为删除操作,"2"现在位于位置[1])。当然,"3"不会被删除,所以你继续按顺序定位[3],现在是"4"。它会被移除,带你到现在的位置[5],也就是"6",依此类推。
删除内容意味着每次执行删除操作时都会跳过某个位置。