如何在迭代时从列表中删除项目?

932
我正在Python中迭代一个元组列表,并尝试根据特定条件删除它们。
for tup in somelist:
    if determine(tup):
         code_to_remove_tup

我应该使用什么替换code_to_remove_tup?我无法弄清楚如何以这种方式删除项目。


本页上的大多数答案并没有真正解释为什么在迭代列表时删除元素会产生奇怪的结果,但是这个问题中被采纳的答案确实做到了,并且对于初次遇到此问题的初学者可能更好。 - ggorlen
25个回答

1093
你可以使用列表推导式创建一个新的列表,其中只包含你不想删除的元素:
somelist = [x for x in somelist if not determine(x)]

或者,通过将赋值给切片somelist [:],您可以更改现有列表以仅包含您想要的项:

somelist[:] = [x for x in somelist if not determine(x)]

如果有其他引用需要反映更改,这种方法可能很有用。

除了理解之外,您还可以使用itertools。在Python 2中:

from itertools import ifilterfalse
somelist[:] = ifilterfalse(determine, somelist)

或在Python 3中:

from itertools import filterfalse
somelist[:] = filterfalse(determine, somelist)

6
如果您只知道少量内容将被删除,即仅删除这些内容并将其他内容保留在原地而不是重写它们,那么您能使它更快吗? - highBandWidth
56
如果我的列表非常庞大,无法承担复制它的成本,该怎么办? - jpcgt
30
你应该使用somelist[:] = (x for x in somelist if determine(x)),这将创建一个生成器,避免不必要的副本。 - Rostislav Kondratenko
9
list_ass_slice()函数实现了somelist[:]=调用,内部使用了PySequence_Fast()。这个函数总是返回一个列表,即@Alex Martelli的解决方案已经使用了列表而不是生成器,很可能更有效 - jfs
9
请问您能否解释一下将列表推导分配给列表和列表克隆之间的区别?在这两种方法中,原始列表somelist不都会被改变吗? - Bowen Liu
显示剩余6条评论

696
建议使用列表推导式的答案几乎是正确的——除了它们会构建一个全新的列表,然后将其命名为旧列表的相同名称,它们并没有就地修改旧列表。这与通过选择性删除进行的不同,例如Lennart的建议 ——它更快,但如果您的列表通过多个引用被访问,则事实上只是重新设置其中一个引用而不是修改列表对象本身,可能会导致微妙且灾难性的错误。
幸运的是,非常容易同时获得列表推导的速度和就地修改所需的语义——只需要编写:
somelist[:] = [tup for tup in somelist if determine(tup)]

注意与其他答案的微妙差别:这个答案并没有给一个裸名赋值。它给一个列表切片赋值,这个切片恰好是整个列表,因此用新的列表内容替换了同一Python列表对象中的列表内容,而不仅仅像其他答案一样重新分配一个引用(从以前的列表对象到新的列表对象)。

1
我该如何在Python 2.6中使用字典进行切片赋值? - PaulMcG
12
@Paul: 由于字典是无序的,对字典进行切片是没有意义的。如果你想用另一个字典 b 中的内容替换字典 a 中的内容,请使用 a.clear(); a.update(b) - Sven Marnach
1
为什么通过更改变量引用的内容来重新设置其中一个引用可能会导致错误?这似乎只有在多线程应用程序中才是潜在问题,而不是单线程应用程序。 - Derek Dahmer
72
@Derek x = ['foo','bar','baz']; y = x; x = [item for item in x if determine(item)]; 这段代码重新将x赋值为列表推导式的结果,但是y仍然引用原始列表['foo','bar','baz']。如果你期望xy引用同一个列表,可能会引入错误。要避免这种情况,可以像Alex展示的那样,对整个列表进行切片赋值,我在这里展示:x = ["foo","bar","baz"]; y = x; x[:] = [item for item in x if determine(item)];。这会直接修改列表,确保所有引用该列表的变量(包括这里的xy)都引用新列表。 - Steven T. Snyder
@Dor:这不起作用,因为您正在迭代“l”并在循环内对其进行变异,而且最重要的是,按照编写方式,您正在重新绑定“l”而不是切片赋值(但是循环是在旧“l”的迭代器上进行的,因此外部循环不会更改)。您应该使用列表推导式替换显式循环(根本不在循环中),例如对于您的情况,您将执行一个“print”循环,然后完成后删除“bad”值。您可能会有for x in l: print x,然后在for循环块的下面和下面执行l[:] = [i for i in l if i!= 3]以删除3。 - ShadowRanger
显示剩余3条评论

395
您需要先复制列表并对其进行迭代,否则迭代可能会失败,并导致意想不到的结果。
例如(具体取决于列表类型):
for tup in somelist[:]:
    etc....

一个例子:

>>> somelist = range(10)
>>> for x in somelist:
...     somelist.remove(x)
>>> somelist
[1, 3, 5, 7, 9]

>>> somelist = range(10)
>>> for x in somelist[:]:
...     somelist.remove(x)
>>> somelist
[]

20
第二个迭代遍历的是列表的副本。因此,当您修改原始列表时,您不会修改所迭代的副本。 - Lennart Regebro
3
在进行一些列表操作时,使用 somelist[:] 比使用 list(somelist) 更好。 - Mariusz Jamro
4
list(somelist)将可迭代对象转换为列表。 somelist[:]将支持切片的对象复制一份。所以它们不一定做相同的事情。在这种情况下,我想复制somelist对象,因此我使用[:] - Lennart Regebro
52
提醒任何阅读此内容的人,这对于列表非常慢。每次迭代时,remove() 必须遍历整个列表,因此这将需要很长时间。 - vitiral
19
当处理仅包含几个项目的列表时,Big O 时间并不重要。通常,对于未来的程序员来说,清晰简明易懂比性能更有价值。 - Steve
显示剩余9条评论

184
for i in range(len(somelist) - 1, -1, -1):
    if some_condition(somelist, i):
        del somelist[i]

你需要倒着循环,否则就像砍断你坐在上面的树枝一样危险 :-)

Python 2用户:将range替换为xrange以避免创建硬编码列表。


20
在Python的最新版本中,您可以通过使用reversed()内置函数更加清晰地实现这一点。 - ncoghlan
20
reversed()不会创建新的列表,而是创建一个反向迭代器以遍历提供的序列。和enumerate()一样,你需要用list()来将其包装起来,从而得到一个列表。或许你在想sorted(),它每次都会创建一个新列表(这是必须的,因为它要排序)。 - ncoghlan
2
这对于数组来说是O(N*M)的,如果你从一个大列表中删除许多项,它会非常慢。因此不建议使用。 - Sam Watkins
2
@SamWatkins 是的,这个答案适用于当你需要从一个非常大的数组中删除几个元素。虽然会减少内存使用,但速度可能会慢 m 倍。 - Navin
2
喜欢这个答案和树枝切割的比喻!列表推导式也可以工作,只要你不需要做任何复杂的事情。唯一的评论是,在Python2中我会使用reversed(xrange(len(somelist))),在Python3中使用reversed(range(len(somelist))) - Barmaley
显示剩余7条评论

74

解决方法概述

要么:

通常情况下,如果您想快速且不想添加自定义的LinkedList类,则默认选择更快的.append()选项,除非内存是一个重要问题。
Python 2官方教程4.2“for Statements”。

https://docs.python.org/2/tutorial/controlflow.html#for-statements

这部分文档明确指出:
  • 您需要复制迭代列表以进行修改
  • 使用切片符号[:]是一种方法
请注意,保留HTML标签。

If you need to modify the sequence you are iterating over while inside the loop (for example to duplicate selected items), it is recommended that you first make a copy. Iterating over a sequence does not implicitly make a copy. The slice notation makes this especially convenient:

>>> words = ['cat', 'window', 'defenestrate']
>>> for w in words[:]:  # Loop over a slice copy of the entire list.
...     if len(w) > 6:
...         words.insert(0, w)
...
>>> words
['defenestrate', 'cat', 'window', 'defenestrate']

Python 2 文档 7.3. "for语句"

https://docs.python.org/2/reference/compound_stmts.html#for

这部分文档再次强调您需要制作副本,并提供了一个实际的删除示例:

Note: There is a subtlety when the sequence is being modified by the loop (this can only occur for mutable sequences, i.e. lists). An internal counter is used to keep track of which item is used next, and this is incremented on each iteration. When this counter has reached the length of the sequence the loop terminates. This means that if the suite deletes the current (or a previous) item from the sequence, the next item will be skipped (since it gets the index of the current item which has already been treated). Likewise, if the suite inserts an item in the sequence before the current item, the current item will be treated again the next time through the loop. This can lead to nasty bugs that can be avoided by making a temporary copy using a slice of the whole sequence, e.g.,

for x in a[:]:
    if x < 0: a.remove(x)

然而,我不同意这种实现方式,因为.remove()必须迭代整个列表才能找到值。
Python是否可以做得更好呢?
看起来,这个特定的Python API可以改进。例如,与以下内容进行比较:
- Java ListIterator::remove,文档中写道“每次调用next或previous只能调用一次此方法” - C++ std::vector::erase,它返回被移除元素之后的有效迭代器
这两种语言都非常清晰地表明,你不能使用除迭代器本身以外的方式修改正在被迭代的列表,并且提供了高效的方法来避免复制列表。

也许背后的理由是Python列表被认为是支持动态数组的,因此任何类型的删除都会效率低下。而Java具有更好的接口层次结构,包括ArrayListLinkedList实现ListIterator

在Python标准库中似乎也没有明确的链表类型:Python Linked List


2
终于有人指出了实际的文档。在此之前,我无法理解任何答案。 - Lukali

53

对于这样的例子,您最好的方法是使用列表推导式

somelist = [tup for tup in somelist if determine(tup)]

在进行比调用 determine 函数更复杂的操作时,我喜欢构造一个新列表并逐步添加元素。例如:

newlist = []
for tup in somelist:
    # lots of code here, possibly setting things up for calling determine
    if determine(tup):
        newlist.append(tup)
somelist = newlist

使用remove复制列表可能会使您的代码看起来更加整洁,如下面的一个答案所述。但是,对于非常大的列表,您绝对不应该这样做,因为这首先涉及复制整个列表,并且对于每个要删除的元素执行O(n)remove操作,使其成为一个O(n^2)算法。

for tup in somelist[:]:
    # lots of code here, possibly setting things up for calling determine
    if determine(tup):
        newlist.append(tup)

41

对于喜欢函数式编程的人:

somelist[:] = filter(lambda tup: not determine(tup), somelist)
或者
from itertools import ifilterfalse
somelist[:] = list(ifilterfalse(determine, somelist))

1
  1. 列表推导式和生成器表达式是从 Haskell 借鉴来的,Haskell 是一种纯函数式语言;它们与 filter 一样函数式,但更符合 Python 的风格。
  2. 如果你需要一个 lambda 来使用 mapfilter,那么列表推导式或生成器表达式 总是 更好的选择;当转换/谓词函数是由 C 实现的 Python 内置函数并且可迭代对象不是非常小的时候,mapfilter 可能会稍微快一些,但当你需要一个 lambda 时,列表推导式或生成器表达式可以避免这种情况,因此总是更快。
- ShadowRanger

24

我需要处理一个非常长的列表,复制这个列表似乎太耗费资源了,特别是考虑到在我的情况下需要删除的项目相对于保留项目数量较少。因此,我采用了这种底层的方法。

array = [lots of stuff]
arraySize = len(array)
i = 0
while i < arraySize:
    if someTest(array[i]):
        del array[i]
        arraySize -= 1
    else:
        i += 1

我不清楚删除一些元素的效率与复制一个大列表的效率相比如何。如果您有任何见解,请评论。


在我的情况下,我需要将那些“不需要的”元素移动到另一个列表中。您对此解决方案有任何新的评论吗?我也认为最好使用一些删除操作而不是复制列表。 - gustavovelascoh
如果性能是一个问题的话,这就是正确的答案(虽然与@Alexey相同)。话虽如此,首先应仔细考虑作为数据结构的list的选择,因为从列表中间删除需要线性时间。如果您不真正需要对第k个连续项进行随机访问,也许可以考虑使用OrderedDict - max
@GVelascoh为什么不创建newlist = [],然后在del array[i]之前添加newlist.append(array[i]) - max
2
请注意,这可能是时间低效的:如果list()是一个链表,那么随机访问是昂贵的;如果list()是一个数组,删除操作是昂贵的,因为它们需要将所有后续元素向前移动。一个不错的迭代器可以使链表实现变得更好。然而,这可能是空间高效的。 - Ciro Santilli OurBigBook.com

23

这里大多数答案都希望你创建列表的副本。但是我有一个使用情况,列表相当长(110K项),保持缩小列表是更明智的选择。

首先,您需要用while循环替换foreach循环

i = 0
while i < len(somelist):
    if determine(somelist[i]):
         del somelist[i]
    else:
        i += 1
<\p>i的值在if块中并没有改变,因为您需要从相同的索引获取新项目的值,一旦旧项目被删除。


1
我不想喜欢这个,但我确实喜欢 :) - anon01
我认为这非常有创意!我希望看到更多社区对这个算法的贡献。它易于理解,但似乎被贡献者忽视了! - tonysepia
1
@tonysepia 很高兴看到这个解决方案仍然有用 :) - Mujeeb
@Mujeeb 哦,是的,你可以在我的算法中看到我使用它:https://dev59.com/aLj4oIgBc1ULPQZFGw7k - tonysepia
1
这并不是普遍最聪明的做法。当然,在某些情况下,这可能是你最好的选择(例如,内存限制紧张),但由于它需要移动许多元素来删除具有大型列表的一个元素,所以它很可能比使用过滤器创建一个新列表要慢得多。不过,如果你不关心列表的顺序,你可以将最后一个元素复制到删除点,然后删除最后一个元素 - 这样做可能比使用过滤器更快。 - Beefster
1
这并不是普遍最聪明的做法。当然,在某些情况下,这可能是你最好的选择(例如,内存限制紧张),但由于它需要移动许多元素来删除具有大型列表的一个元素,所以它可能比使用过滤器创建一个新列表要慢得多。不过,如果你不关心列表的顺序,你可以将最后一个元素复制到删除点,然后删除最后一个元素 - 这样做可能比使用过滤器更快。 - undefined

12

如果当前列表项符合所需条件,创建一个新列表可能是明智的选择。

因此:

for item in originalList:
   if (item != badValue):
        newList.append(item)

为了避免必须使用新的列表名称重新编写整个项目:

originalList[:] = newList

注意,以下内容来自Python文档:

copy.copy(x) 返回x的浅复制。

copy.deepcopy(x) 返回x的深度复制。


4
这并没有提供任何新的信息,它与多年前被接受的答案没有区别。 - Mark Amery
2
这很简单,只是另一种看待问题的方式@MarkAmery。对于那些不喜欢压缩编码语法的人来说,它更少被压缩。 - ntk4

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