在遍历列表时删除元素
以下代码:
a = list(range(10))
remove = False
for b in a:
if remove:
a.remove(b)
remove = not remove
print(a)
在使用 Python 3.2 时,输出的是 [0, 2, 3, 5, 6, 8, 9]
,而不是 [0, 2, 4, 6, 8]
。
- 为什么会输出这些特定的值?
- 为什么没有错误提示说明底层的迭代器正在被修改?
- 这种行为在早期版本的 Python 中有没有变化?
请注意,我并不是想寻找解决方法,而是想理解这个问题。
5 个回答
当然,在遍历一个数组的时候去修改它是很危险的。规范上说这样做是不好的,结果是不可预测的:
http://docs.python.org/tutorial/controlflow.html#for-statements
那么,下一个问题是,这里到底发生了什么呢?如果让我猜,我会说它的内部机制大概是这样的:
for(int i=0; i<len(array); ++i)
{
do_loop_body(i);
}
如果你认为这确实是发生的事情,那就能完全解释观察到的行为。当你在当前指针位置或之前的位置删除一个元素时,整个列表会向左移动一位。第一次,你像往常一样删除一个1,但这时列表向后移动了。下一次迭代时,你不是碰到2,而是碰到3。然后你删除一个4,列表又向后移动。接下来迭代时碰到7,依此类推。
正如Mikola所解释的,你看到的实际结果是因为从列表中删除一个条目会导致整个列表向左移动一个位置,这样就会漏掉一些元素。
但我觉得更有趣的问题是,为什么Python在发生这种情况时不产生错误信息。其实如果你尝试修改字典,Python是会给出错误提示的。我认为这有两个原因。
字典内部结构比较复杂,而列表则简单得多。列表基本上就是数组。字典在被遍历时需要检测是否被修改,以避免在内部结构变化时崩溃。而列表可以不进行这种检查,因为它只需要确保当前的索引仍然在有效范围内。
从历史上看(我不确定现在是否如此),Python列表是通过使用[]操作符来遍历的。Python会依次评估list[0]、list[1]、list[2],直到遇到IndexError。在这种情况下,Python在开始时并没有跟踪列表的大小,所以它没有办法检测到列表的大小已经发生变化。
我考虑了很久要不要回答这个问题,因为类似的问题在这里已经问过很多次了。不过这个问题还是有点独特,值得给它一个机会。(不过,如果其他人投票关闭,我也不会反对。)下面是对发生情况的一个视觉解释。
[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
^
既然没有其他人回答,我就来尝试回答你其他的问题:
为什么没有错误提示说底层的迭代器正在被修改?
如果要在不禁止许多有效的循环结构的情况下抛出错误,Python就需要知道很多事情,而且这些信息可能得在运行时获取。获取这些信息会花费时间,这样一来,Python的速度就会变慢,而速度在循环中是非常重要的。
这种行为在早期版本的Python中有变化吗?
简而言之,没有。或者说我非常怀疑有变化,至少自从我学习Python(2.4)以来,它一直是这样工作的。老实说,我认为任何简单实现的可变序列都会这样表现。如果有人知道得更多,请纠正我。(实际上,快速查阅文档确认,Mikola引用的内容自1.4版本以来就已经在教程中出现了!)