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

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

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


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

11

这个答案最初是回答一个已被标记为重复的问题而撰写的: 从Python中删除列表中的坐标

你的代码有两个问题:

1) 当使用remove()函数时,你试图删除整数,而你需要删除元组。

2) for循环将跳过你列表中的某些项。

让我们来看一下当我们执行你的代码时会发生什么:

>>> L1 = [(1,2), (5,6), (-1,-2), (1,-2)]
>>> for (a,b) in L1:
...   if a < 0 or b < 0:
...     L1.remove(a,b)
... 
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
TypeError: remove() takes exactly one argument (2 given)
第一个问题是你将'a'和'b'同时传递给remove()方法,但remove()方法只接受一个参数。所以我们该如何让remove()方法正确地处理你的列表呢?我们需要弄清楚列表中每个元素是什么。在这种情况下,每个元素都是一个元组。为了看到这一点,让我们访问列表中的一个元素(索引从0开始):
>>> L1[1]
(5, 6)
>>> type(L1[1])
<type 'tuple'>

啊哈!L1的每个元素实际上都是一个元组。所以这就是我们需要传递给remove()的东西。在Python中,元组非常容易,它们只是将值括在括号内。"a,b"不是元组,但"(a,b)"是一个元组。所以我们修改你的代码并再次运行:

# The remove line now includes an extra "()" to make a tuple out of "a,b"
L1.remove((a,b))

这段代码没有任何错误,但让我们看一下它输出的列表:

L1 is now: [(1, 2), (5, 6), (1, -2)]

为什么你的列表中仍然包含(1,-2)?原来是在使用循环遍历时修改列表是一个非常不好的想法,需要特别小心。(1,-2) 保留在列表中的原因是在for循环迭代之间每个项目的位置发生了变化。让我们看一下如果我们给上面的代码提供一个更长的列表会发生什么:

L1 = [(1,2),(5,6),(-1,-2),(1,-2),(3,4),(5,7),(-4,4),(2,1),(-3,-3),(5,-1),(0,6)]
### Outputs:
L1 is now: [(1, 2), (5, 6), (1, -2), (3, 4), (5, 7), (2, 1), (5, -1), (0, 6)]

从那个结果可以推断出,每当条件语句评估为true并且列表项被删除时,循环的下一次迭代将跳过对列表中下一项的评估,因为其值现在位于不同的索引位置。

最直观的解决方法是复制列表,然后迭代原始列表并仅修改副本。您可以尝试这样做:

L2 = L1
for (a,b) in L1:
    if a < 0 or b < 0 :
        L2.remove((a,b))
# Now, remove the original copy of L1 and replace with L2
print L2 is L1
del L1
L1 = L2; del L2
print ("L1 is now: ", L1)

然而,输出结果将与之前相同:

'L1 is now: ', [(1, 2), (5, 6), (1, -2), (3, 4), (5, 7), (2, 1), (5, -1), (0, 6)]

这是因为当我们创建 L2 时,Python 实际上并没有创建一个新对象。相反,它仅仅将 L2 引用到与 L1 相同的对象。我们可以使用 'is' 进行验证,这与仅仅“相等”(==)不同。

>>> L2=L1
>>> L1 is L2
True

我们可以使用copy.copy()创建真正的副本,然后一切都按照预期工作:

import copy
L1 = [(1,2), (5,6),(-1,-2), (1,-2),(3,4),(5,7),(-4,4),(2,1),(-3,-3),(5,-1),(0,6)]
L2 = copy.copy(L1)
for (a,b) in L1:
    if a < 0 or b < 0 :
        L2.remove((a,b))
# Now, remove the original copy of L1 and replace with L2
del L1
L1 = L2; del L2
>>> L1 is now: [(1, 2), (5, 6), (3, 4), (5, 7), (2, 1), (0, 6)]

最后,有一个比完全复制L1更为简洁的解决方案:reversed()函数:

L1 = [(1,2), (5,6),(-1,-2), (1,-2),(3,4),(5,7),(-4,4),(2,1),(-3,-3),(5,-1),(0,6)]
for (a,b) in reversed(L1):
    if a < 0 or b < 0 :
        L1.remove((a,b))
print ("L1 is now: ", L1)
>>> L1 is now: [(1, 2), (5, 6), (3, 4), (5, 7), (2, 1), (0, 6)]

很遗憾,我无法充分描述reversed()的工作原理。当一个列表被传递给它时,它返回一个'listreverseiterator'对象。出于实际目的,您可以将其视为创建其参数的反向副本。这是我推荐的解决方案。


9
如果您想在迭代过程中删除列表中的元素,请使用while循环,以便在每次删除后更改当前索引和结束索引。
示例:
i = 0
length = len(list1)

while i < length:
    if condition:
        list1.remove(list1[i])
        i -= 1
        length -= 1

    i += 1

8
其他答案已经指出,在迭代列表时删除元素通常是不明智的。反向迭代可以避免一些问题,但是这样做会使代码更难理解,因此通常最好使用列表推导式或 filter。然而,在只有在迭代过程中仅删除一个元素时,才可以安全地从正在迭代的序列中删除元素。这可以通过使用 returnbreak 来确保。例如:
for i, item in enumerate(lst):
    if item % 4 == 0:
        foo(item)
        del lst[i]
        break

当你需要在符合某个条件的列表中对第一个元素进行带有副作用的操作,然后立即将该元素从列表中删除时,使用生成器表达式往往比列表推导式更易于理解。


5

如果您不仅想删除一些东西,还想在单个循环中对所有元素进行操作,那么有一个可能的解决方案:

alist = ['good', 'bad', 'good', 'bad', 'good']
i = 0
for x in alist[:]:
    if x == 'bad':
        alist.pop(i)
        i -= 1
    # do something cool with x or just print x
    print(x)
    i += 1

你应该使用推导式,它们更容易理解。 - Beefster
1
如果我想在一个循环中删除“坏”东西,对其进行某些操作,并且还要对“好”东西进行某些操作,该怎么办? - Alexey
1
其实,我意识到这里有一些巧妙的地方,就是你用一个开放切片来复制列表(alist[:])。而且,由于你可能正在做一些花哨的事情,它实际上具有使用案例。好的修订是好的。顶你一个赞。 - Beefster

5

一个for循环将会遍历一个索引...

假设你有一个列表,

[5, 7, 13, 29, 65, 91]

你使用了一个名为lis的列表变量。你使用它来删除...。
你的变量。
lis = [5, 7, 13, 29, 35, 65, 91]
       0  1   2   3   4   5   6

在第5次迭代期间, 您的数字35不是质数,因此您将其从列表中删除。
lis.remove(y)

然后下一个值(65)移到前一个索引。
lis = [5, 7, 13, 29, 65, 91]
       0  1   2   3   4   5

因此第四次迭代完成后指针移动到第五个...

这就是为什么你的循环不包括65,因为它已经移动到了前一个索引。

所以你不应该将列表引用到另一个变量中,该变量仍然引用原始列表而不是副本。

ite = lis # Don’t do it will reference instead copy

现在,您需要使用list[::]复制该列表。

接下来,您将会得到:

[5, 7, 13, 29]

问题在于您在迭代过程中从列表中删除了一个值,然后您的列表索引会崩溃。
因此,您可以尝试使用列表推导式
它支持所有可迭代的对象,如列表、元组、字典、字符串等。

简单来说:不要在你试图更改的列表上进行迭代。相反,应该在具有需要被删除标准的项目的列表上进行迭代:lis = [5, 7, 13, 29, 35, 65, 91] not_primes = [35,65] for item in not_primes: if item in lis: lis.remove(item) 我自己也遇到了这个问题,在这里讨论过:https://stackoverflow.com/q/72478091/1973308 - Hank Lenzi

5

如果您在迭代期间需要执行其他操作,获取索引(这可以确保您能够引用它,例如如果您有一个字典列表)和实际的列表项内容可能是很好的选择。

inlist = [{'field1':10, 'field2':20}, {'field1':30, 'field2':15}]    
for idx, i in enumerate(inlist):
    do some stuff with i['field1']
    if somecondition:
        xlist.append(idx)
for i in reversed(xlist): del inlist[i]

enumerate 可以同时让你访问元素和索引。使用 reversed 是为了防止之后要删除的索引位置变化。


为什么在拥有字典列表的情况下获取索引比其他类型的列表更重要?就我所知,这没有任何意义。 - Mark Amery

4

您可能想要使用内置的filter()函数来进行过滤。

更多细节请查看这里


3
我需要做类似的事情,我的问题是内存 - 我需要将列表中的多个数据集对象合并为一个新对象,并在对它们进行一些处理后,需要摆脱每个我正在合并的条目,以避免复制所有条目并使内存爆炸。在我的情况下,将对象放在字典中而不是列表中可以很好地解决这个问题:

```

k = range(5)
v = ['a','b','c','d','e']
d = {key:val for key,val in zip(k, v)}

print d
for i in range(5):
    print d[i]
    d.pop(i)
print d

```


3
您可以尝试反向for循环,例如针对some_list,您可以执行以下操作:
list_len = len(some_list)
for i in range(list_len):
    reverse_i = list_len - 1 - i
    cur = some_list[reverse_i]

    # some logic with cur element

    if some_condition:
        some_list.pop(reverse_i)

这样索引就会对齐,并且不会受到列表更新的影响(无论您是否弹出当前元素)。


循环遍历 reversed(list(enumerate(some_list))) 比自己计算索引更简单。 - Mark Amery
@MarkAmery,我认为你不能用这种方式更改列表。 - Queequeg

3

最有效的方法是使用列表推导式,许多人展示了他们的案例,当然,通过filter获取iterator也是一个不错的方法。

filter接收一个函数和一个序列。 filter依次将传递的函数应用于每个元素,然后根据函数返回值是否为TrueFalse来决定是否保留或丢弃元素。

下面是一个示例(从元组中获取奇数):

list(filter(lambda x:x%2==1, (1, 2, 4, 5, 6, 9, 10, 15)))  
# result: [1, 5, 9, 15]

注意:您也不能处理迭代器。有时候迭代器比序列更好。

我认为这可能是从列表中删除项目的最惯用方式。由于应用程序未改变变量,因此此行为也将是线程安全的。 - Supreet Sethi

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