从列表中删除项 - 迭代期间 - 这个习语有什么问题?

30

我做了一个实验,具体如下:

letters=['a','b','c','d','e','f','g','h','i','j','k','l']
for i in letters:
    letters.remove(i)
print letters
最后一次打印显示并非所有项目都被移除了?(每隔一个被移除)。
IDLE 2.6.2      
>>> ================================ RESTART ================================
>>> 
['b', 'd', 'f', 'h', 'j', 'l']
>>> 

这是什么原理?如何重写以删除每个项目?


4
怎样翻译呢?letters = []? :-) - paxdiablo
9个回答

48
一些答案解释了为什么会发生这种情况,一些则解释了你应该做什么。我将不遗余力地把它们整合在一起。

这是为什么呢?因为 Python 语言被设计成能够以不同的方式处理这种用例。文档明确指出:(文档)
修改正在迭代循环的序列是不安全的(这只适用于可变序列类型,例如列表)。如果需要修改正在迭代的列表(例如复制所选项目),则必须迭代副本
强调属于我,有关更多信息,请参见链接页面--该文档已受版权保护,保留所有权利。
你可以很容易理解为什么会得到你得到的结果,但它基本上是未定义行为,可能会在不同版本的构建中毫无警告地轻松更改。别这样做。
这就像想知道为什么i += i++ + ++i在你的架构、编译器特定版本和语言中会做出奇怪的行为--包括但不限于破坏你的计算机使恶魔从你的鼻子里飞出来 :)

要如何重写以删除所有项目?
- del letters [:](如果需要更改对此对象的所有引用)
- letters [:] = [](如果需要更改对此对象的所有引用)
- letters = [](如果只想使用新对象)
也许你只想根据条件移除一些项目?在这种情况下,你应该迭代列表的副本。最简单的方法是使用包含整个列表的切片:[:]语法,例如:
#remove unsafe commands
commands = ["ls", "cd", "rm -rf /"]
for cmd in commands[:]:
  if "rm " in cmd:
    commands.remove(cmd)

如果您的检查不是特别复杂的话,您可以(并且可能应该)使用过滤器:

commands = [cmd for cmd in commands if not is_malicious(cmd)]

优秀的“毁掉你的编译器”的内容 - Chris McCall
我不会说“Python源代码没有考虑到此用例。” 这与语言的设计方式有关,而不是编码的特定方面。 - Andrew Jaffe
1
@Andrew:嗯——由于列表在迭代时是不可变的,因此您无需在代码中处理迭代期间您的错误突变的情况。以某种倒退的方式,它使得... 呃...好吧,我已经重新表述了。 - badp
1
这个答案引用了Python 2.6教程的“第4.2节:for语句”(也适用于3.0和3.1版本)。自那时以来,Python 2.7版本(以及3.2+)已经将语言软化为“如果您需要在循环内部修改正在迭代的序列(例如复制所选项目),建议您首先进行复制。”它可以被正确地完成,但是它很棘手,通常会导致不愉快的惊喜。 - Kevin J. Chase
请注意,最后一个示例创建了一个新列表。如果在此语句之前某个其他变量指向“commands”,则它现在指向与“commands”不同的对象,您无法再使用该变量修改“commands”。 - Cyker

14

你不能同时迭代一个列表并进行修改,相反应该迭代一个切片:

letters=['a','b','c','d','e','f','g','h','i','j','k','l']
for i in letters[:]: # note the [:] creates a slice
     letters.remove(i)
print letters

话虽如此,对于这样简单的操作,你应该简单地使用:

letters = []

8

如果你修改正在迭代的列表,那么你会得到奇怪的结果。为了避免这种情况发生,你必须迭代列表的副本:

for i in letters[:]:
  letters.remove(i)

6

它会移除第一个出现的数字,然后检查序列中的下一个数字。由于序列已经改变,它会取下一个奇数数字再进行操作...

  • 取出 "a"
  • 移除 "a" -> 第一个数字现在是 "b"
  • 取出下一个数字,"c" -...

5
你想要做的是:
letters[:] = []

或者

del letters[:]

这样会保留原始对象letters所指向的内容。其他选项如 letters = [],则会创建一个新对象并将letters指向它:旧对象通常在一段时间后被垃圾回收。

未删除所有值的原因是您在迭代列表时更改了它。

ETA: 如果您想从列表中过滤值,可以使用列表推导式,例如:

>>> letters=['a','b','c','d','e','f','g','h','i','j','k','l']
>>> [l for l in letters if ord(l) % 2]
['a', 'c', 'e', 'g', 'i', 'k']

这个解释对我来说很有道理(列表是可变的);删除方法对于一个完整的列表来说是可以的,但是(一如既往)给出的例子是一个简化版本的真实问题:有条件地从列表中移除项目;所以我想在开始之前只需复制原始列表。 - monojohnny

1
    #!/usr/bin/env python
    import random
    a=range(10)

    while len(a):
        print a
        for i in a[:]:
            if random.random() > 0.5:
                print "removing: %d" % i
                a.remove(i)
            else:
                print "keeping: %d"  % i           

    print "done!"
    a=range(10)

    while len(a):
        print a
        for i in a:
            if random.random() > 0.5:
                print "removing: %d" % i
                a.remove(i)
            else:
                print "keeping: %d"  % i           

    print "done!"

我认为这更好地解释了问题,顶部的代码块可行,而底部的则不行。
因为您正在修改正在迭代的列表,所以“保留”在底部列表中的项目永远不会被打印出来,这是一种灾难性的做法。

1
可能Python使用指针,删除从前开始。第二行的变量“letters”部分与第三行的变量“letters”值不同。当i为1时,a被移除;当i为2时,b被移动到位置1,c被移除。你可以尝试使用“while”循环。

0

最初,i 是对数组 a 的引用。随着循环的进行,第一个位置的元素被删除或移除,第二个位置的元素占据了第一个位置,但指针移动到了第二个位置。这种情况会一直持续下去,这就是为什么我们不能删除 b,d,f,h,j,l 的原因。

`


0

好的,我来晚了一点,但是我一直在思考这个问题,在查看Python(CPython)的实现代码后,我有一个我喜欢的解释。如果有人知道为什么它很傻或者错误,我希望能听到为什么。

问题是使用迭代器遍历列表,同时允许该列表发生更改。

所有迭代器需要做的就是告诉你在当前项之后(即使用next()函数)哪个项目在(在这种情况下)列表中。

我认为目前实现迭代器的方式是,它们只跟踪它们迭代过的最后一个元素的索引。在iterobject.c中查看可以看到似乎是迭代器的定义:

typedef struct {
    PyObject_HEAD
    Py_ssize_t it_index;
    PyObject *it_seq; /* Set to NULL when iterator is exhausted */
} seqiterobject;

it_seq 指向正在迭代的序列,it_index 给出了迭代器提供的最后一项的索引。

当迭代器刚刚提供了第 n 个项目并从序列中删除该项目时,后续列表元素及其索引之间的对应关系会发生变化。前 (n+1) 个项目成为迭代器关注的第 n 个项目。换句话说,迭代器现在认为序列中的“下一个”项目实际上是“当前”项目。

因此,当要求提供下一个项目时,它将提供前 (n+2) 个项目(即新的 (n+1) 个项目)。

因此,对于所讨论的代码,迭代器的 next() 方法仅会从原始列表中提供 n+0、n+2、n+4 等元素。n+1、n+3、n+5 等项目永远不会暴露给 remove 语句。

尽管问题代码的预期活动很清晰(至少对于人来说),但迭代器要监视其迭代的序列中的更改,然后以“人类”方式采取行动可能需要更多的内省。

如果迭代器可以返回先前或当前序列的元素,则可能会有一般性解决方法,但由于现在的情况是,您需要迭代列表的副本,并确保在迭代器到达它们之前不删除任何项目。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接