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

我正在迭代Python中的元组列表,如果它们符合某些条件,我将尝试删除它们。

for tup in somelist:if determine(tup):code_to_remove_tup

我应该用什么来代替code_to_remove_tup?我不知道如何以这种方式删除项目。

810590 次浏览

您需要获取列表的副本并首先对其进行迭代,否则迭代将失败并可能导致意外结果。

例如(取决于列表的类型):

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[]

对于这样一个例子,你最好的方法是列表理解

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 determineif 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 determineif determine(tup):newlist.append(tup)

您可以使用列表推导创建一个新列表,其中仅包含您不想删除的元素:

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

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

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

如果有其他对somelist的引用需要反映更改,则此方法可能很有用。

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

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

或者在Python 3中:

from itertools import filterfalsesomelist[:] = filterfalse(determine, somelist)
for i in range(len(somelist) - 1, -1, -1):if some_condition(somelist, i):del somelist[i]

你需要向后走,否则就有点像锯断你坐的树枝:-)

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

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

somelist[:] = filter(lambda tup: not determine(tup), somelist)

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

建议列表推导的答案几乎是正确的-除了他们构建一个全新的列表,然后给它一个与旧列表相同的名称,他们不修改旧列表。这与您通过选择性删除所做的不同,如@Lennart的建议-它更快,但如果您的列表是通过多个引用访问的,您只是重置其中一个引用而不是更改列表对象本身的事实可能会导致微妙的灾难性错误。

幸运的是,它非常容易获得列表理解的速度和就地更改所需的语义学-只需代码:

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

注意与其他答案的细微区别:这个答案不是分配给barename-它分配给恰好是整个列表的列表切片,从而替换列表内容在同一个Python列表对象中,而不是像其他答案一样只是重新引用一个引用(从以前的列表对象到新的列表对象)。

您可能希望使用filter()可用作为内置。

更多详情检查这里

如果您想在迭代期间做任何其他事情,最好同时获取索引(这可以保证您能够引用它,例如,如果您有一个dicts列表)和实际的列表项内容。

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是为了让您稍后要删除的索引不会对您进行更改。

你可以尝试反向循环,所以对于some_list你会这样做:

list_len = len(some_list)for i in range(list_len):reverse_i = list_len - 1 - icur = some_list[reverse_i]
# some logic with cur element
if some_condition:some_list.pop(reverse_i)

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

变通方法概述

要么:

一般来说,如果你做的又快又脏,不想添加一个自定义的LinkedList类,默认情况下你只想选择更快的.append()选项,除非内存是一个大问题。

官方Python 2教程4.2。“for语句”

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

文档的这一部分明确指出:

  • 您需要复制迭代列表以修改它
  • 一种方法是使用切片符号[:]

如果您需要在循环内修改迭代的序列(例如复制选定的项目),建议您首先复制。迭代序列不会隐式复制。切片表示法使这一点特别方便:

>>> 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

文档的这一部分再次说明您必须复制,并给出了一个实际的删除示例:

注意:循环修改序列时有一个微妙之处(这只发生在可变序列中,即列表)。一个内部计数器用于跟踪下一个使用的项目,并在每次迭代中递增。当这个计数器达到序列的长度时,循环终止。这意味着如果套件从序列中删除当前(或上一个)项目,下一个项目将被跳过(因为它获取已经处理过的当前项目的索引)。类似地,如果套件在当前项之前插入序列中的项,则下次通过循环时将再次处理当前项。这可能会导致严重的错误,可以通过使用整个序列的切片进行临时复制来避免这些错误,例如,

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

但是,我不同意这种实现,因为.remove()必须迭代整个列表才能找到值。

Python能做得更好吗?

这个特定的Python API似乎可以改进。例如,将其与以下内容进行比较:

两者都清楚地表明,除了迭代器本身之外,您不能修改正在迭代的列表,并为您提供了在不复制列表的情况下这样做的有效方法。

也许潜在的理由是Python列表被假定为动态数组支持,因此任何类型的删除都将是时间低效的,而Java有一个更好的接口层次结构,ArrayListLinkedList实现ListIterator

在Python stdlib中似乎也没有显式的链表类型:Python链表

我需要做一些类似的事情,在我的情况下,问题是内存——我需要在一个列表中合并多个数据集对象,在对它们做了一些事情之后,作为一个新对象,并且需要摆脱我合并的每个条目,以避免复制所有这些条目并破坏内存。

"'

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

"'

如果当前列表项满足所需条件,则仅创建一个新列表可能是明智的。

所以:

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

并避免使用新列表名称重新编码整个项目:

originalList[:] = newList

注意,来自Python留档:

copy.copy(x)返回x的浅拷贝。

copy.deepcopy(x)返回x的深层副本。

TLDR:

我写了一个库,可以让你这样做:

from fluidIter import FluidIterablefSomeList = FluidIterable(someList)for tup in fSomeList:if determine(tup):# remove 'tup' without "breaking" the iterationfSomeList.remove(tup)# tup has also been removed from 'someList'# as well as 'fSomeList'

如果可能,最好使用另一种方法,该方法在迭代时不需要修改你的可迭代对象,但对于某些算法,它可能不是那么直接。因此,如果你确定你真的想要最初问题中描述的代码模式,这是可能的。

应该适用于所有可变序列,而不仅仅是列表。


完整答案:

编辑:此答案中的最后一个代码示例给出了为什么的用例,您有时可能希望就地修改列表而不是使用列表理解。答案的第一部分作为如何的教程,可以就地修改数组。

该解决方案遵循senderle的这个答案(针对相关问题)。这解释了在迭代已修改的列表时如何更新数组索引。下面的解决方案旨在即使列表被修改也能正确跟踪数组索引。

这里https://github.com/alanbacon/FluidIterator下载fluidIter.py,它只是一个文件,因此无需安装git。没有安装程序,因此您需要确保该文件位于您自己的python路径中。该代码已为python 3编写,未经测试在python 2上。

from fluidIter import FluidIterablel = [0,1,2,3,4,5,6,7,8]fluidL = FluidIterable(l)for i in fluidL:print('initial state of list on this iteration: ' + str(fluidL))print('current iteration value: ' + str(i))print('popped value: ' + str(fluidL.pop(2)))print(' ')
print('Final List Value: ' + str(l))

这将产生以下产出:

initial state of list on this iteration: [0, 1, 2, 3, 4, 5, 6, 7, 8]current iteration value: 0popped value: 2
initial state of list on this iteration: [0, 1, 3, 4, 5, 6, 7, 8]current iteration value: 1popped value: 3
initial state of list on this iteration: [0, 1, 4, 5, 6, 7, 8]current iteration value: 4popped value: 4
initial state of list on this iteration: [0, 1, 5, 6, 7, 8]current iteration value: 5popped value: 5
initial state of list on this iteration: [0, 1, 6, 7, 8]current iteration value: 6popped value: 6
initial state of list on this iteration: [0, 1, 7, 8]current iteration value: 7popped value: 7
initial state of list on this iteration: [0, 1, 8]current iteration value: 8popped value: 8
Final List Value: [0, 1]

上面我们在流体列表对象上使用了pop方法。其他常见的可迭代方法也被实现,例如del fluidL[i].remove.insert.append.extend。列表也可以使用切片修改(sortreverse方法没有实现)。

唯一的条件是,如果在任何时候fluidLl被重新分配给不同的列表对象,您必须只在原地修改列表,代码将不起作用。原始的fluidL对象仍将被for循环使用,但会超出我们修改的范围。

fluidL[2] = 'a'   # is OKfluidL = [0, 1, 'a', 3, 4, 5, 6, 7, 8]  # is not OK

如果我们想访问列表的当前索引值,我们不能使用enumerate,因为这只计算for循环运行的次数。相反,我们将直接使用迭代器对象。

fluidArr = FluidIterable([0,1,2,3])# get iterator first so can query the current indexfluidArrIter = fluidArr.__iter__()for i, v in enumerate(fluidArrIter):print('enum: ', i)print('current val: ', v)print('current ind: ', fluidArrIter.currentIndex)print(fluidArr)fluidArr.insert(0,'a')print(' ')
print('Final List Value: ' + str(fluidArr))

这将输出以下内容:

enum:  0current val:  0current ind:  0[0, 1, 2, 3]
enum:  1current val:  1current ind:  2['a', 0, 1, 2, 3]
enum:  2current val:  2current ind:  4['a', 'a', 0, 1, 2, 3]
enum:  3current val:  3current ind:  6['a', 'a', 'a', 0, 1, 2, 3]
Final List Value: ['a', 'a', 'a', 'a', 0, 1, 2, 3]

FluidIterable类只是为原始列表对象提供了一个包装器。原始对象可以作为流体对象的属性访问,如下所示:

originalList = fluidArr.fixedIterable

更多示例/测试可以在fluidIter.py底部的if __name__ is "__main__":部分找到。这些值得一看,因为它们解释了在各种情况下会发生什么。例如:使用切片替换列表的大部分。或者在嵌套for循环中使用(和修改)相同的可迭代对象。

正如我在开始时所说:这是一个复杂的解决方案,会损害代码的易读性并使其更难调试。因此,应该首先考虑其他解决方案,例如David Raznick回答中提到的列表推导。话虽如此,我发现这个类对我很有用,并且比跟踪需要删除的元素的索引更容易使用。


编辑:正如评论中提到的,这个答案并没有真正提出这种方法可以解决的问题。我将在这里尝试解决这个问题:

列表推导提供了一种生成新列表的方法,但这些方法倾向于孤立地查看每个元素,而不是整个列表的当前状态。

newList = [i for i in oldList if testFunc(i)]

但是,如果testFunc的结果取决于已经添加到newList中的元素呢?或者仍然在oldList中的元素可能会被添加到下一个?可能仍然有一种使用列表理解的方法,但它将开始失去它的优雅,对我来说,修改列表更容易。

下面的代码是遭受上述问题的算法的一个示例。该算法将减少列表,使任何元素都不是任何其他元素的倍数。

randInts = [70, 20, 61, 80, 54, 18, 7, 18, 55, 9]fRandInts = FluidIterable(randInts)fRandIntsIter = fRandInts.__iter__()# for each value in the list (outer loop)# test against every other value in the list (inner loop)for i in fRandIntsIter:print(' ')print('outer val: ', i)innerIntsIter = fRandInts.__iter__()for j in innerIntsIter:innerIndex = innerIntsIter.currentIndex# skip the element that the outloop is currently on# because we don't want to test a value against itselfif not innerIndex == fRandIntsIter.currentIndex:# if the test element, j, is a multiple# of the reference element, i, then remove 'j'if j%i == 0:print('remove val: ', j)# remove element in place, without breaking the# iteration of either loopdel fRandInts[innerIndex]# end if multiple, then remove# end if not the same value as outer loop# end inner loop# end outerloop
print('')print('final list: ', randInts)

输出和最终缩小列表如下所示

outer val:  70
outer val:  20remove val:  80
outer val:  61
outer val:  54
outer val:  18remove val:  54remove val:  18
outer val:  7remove val:  70
outer val:  55
outer val:  9remove val:  18
final list:  [20, 61, 7, 55, 9]

一个可能的解决方案,如果你不仅想删除一些东西,还想在一个循环中对所有元素做一些事情,这很有用:

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

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

你的代码中有两个问题:

1)当使用删除()时,您尝试删除整数,而您需要删除元组。

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'给删除(),但删除()只接受一个参数。那么我们如何让删除()在你的列表中正常工作?我们需要弄清楚列表中的每个元素是什么。在这种情况下,每个元素都是一个元组。为了看到这一点,让我们访问列表中的一个元素(索引从0开始):

>>> L1[1](5, 6)>>> type(L1[1])<type 'tuple'>

啊哈!L1的每个元素实际上是一个元组。所以这就是我们需要传递来删除()的内容。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 = L1for (a,b) in L1:if a < 0 or b < 0 :L2.remove((a,b))# Now, remove the original copy of L1 and replace with L2print L2 is L1del L1L1 = L2; del L2print ("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 L2True

我们可以使用copy.copy()制作一个真实的副本。然后一切都按预期工作:

import copyL1 = [(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 L2del L1L1 = L2; del L2>>> L1 is now: [(1, 2), (5, 6), (3, 4), (5, 7), (2, 1), (0, 6)]

最后,有一个比创建一个全新的L1副本更干净的解决方案。反向()函数:

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)]

不幸的是,我无法充分描述反向()是如何工作的。当列表传递给它时,它会返回一个“listrverseiterator”对象。出于实际目的,您可以将其视为创建其参数的反向副本。这是我推荐的解决方案。

我需要用一个巨大的列表来做这件事,复制这个列表似乎很昂贵,特别是在我的情况下,与剩下的项目相比,删除的数量很少。

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

我不知道的是,与复制一个大列表相比,几次删除的效率有多高。如果您有任何见解,请发表评论。

其他答案是正确的,从正在迭代的列表中删除通常是个坏主意。反向迭代避免了一些陷阱,但要遵循这样做的代码要困难得多,所以通常你最好使用列表理解或filter

但是,有一种情况可以安全地从您正在迭代的序列中删除元素:如果您在迭代时只删除一个项目。这可以使用returnbreak来确保。例如:

for i, item in enumerate(lst):if item % 4 == 0:foo(item)del lst[i]break

这通常比列表理解更容易理解,当你对列表中满足某些条件的第一个项目执行一些副作用的操作时,然后立即从列表中删除该项目。

对于任何有潜力成为真正大的东西,我使用以下方法。

import numpy as np
orig_list = np.array([1, 2, 3, 4, 5, 100, 8, 13])
remove_me = [100, 1]
cleaned = np.delete(orig_list, remove_me)print(cleaned)

这应该比其他任何事情都快得多。

在某些情况下,您所做的不仅仅是一次过滤一个列表项,您希望您的迭代在迭代时发生变化。

这里有一个例子,事先复制列表是不正确的,反向迭代是不可能的,列表理解也不是一个选项。

""" Sieve of Eratosthenes """
def generate_primes(n):""" Generates all primes less than n. """primes = list(range(2,n))idx = 0while idx < len(primes):p = primes[idx]for multiple in range(p+p, n, p):try:primes.remove(multiple)except ValueError:pass #EAFPidx += 1yield p

这里的大多数答案都希望您创建列表的副本。我有一个用例,其中列表很长(110K项),继续减少列表更明智。

首先你需要将Foreach循环替换为况且循环

i = 0while i < len(somelist):if determine(somelist[i]):del somelist[i]else:i += 1

i的值不会在if块中更改,因为一旦旧项被删除,您将希望从相同索引中获取新项的值。

最有效的方法是列表理解,许多人展示了他们的案例,当然,这也是获得iteratorfilter的好方法。

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

有一个例子(在元组中获取赔率):

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

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

我可以想到三种方法来解决你的问题。例如,我将创建一个随机元组列表somelist = [(1,2,3), (4,5,6), (3,6,6), (7,8,9), (15,0,0), (10,11,12)]。我选择的条件是sum of elements of a tuple = 15。在最终列表中,我们将只有总和不等于15的元组。

我选择的是一个随机选择的例子。随意更改元组列表和我选择的条件

方法1.>使用你建议的框架(在for循环中填写代码)。我使用带有del的小代码来删除满足上述条件的元组。但是,如果两个连续放置的元组满足给定条件,此方法将错过一个元组(满足上述条件)。

for tup in somelist:if ( sum(tup)==15 ):del somelist[somelist.index(tup)]
print somelist>>> [(1, 2, 3), (3, 6, 6), (7, 8, 9), (10, 11, 12)]

方法2.>构造一个新列表,其中包含不满足给定条件的元素(元组)(这与删除满足给定条件的列表元素相同)。以下是代码:

newlist1 = [somelist[tup] for tup in range(len(somelist)) if(sum(somelist[tup])!=15)]
print newlist1>>>[(1, 2, 3), (7, 8, 9), (10, 11, 12)]

方法3.>找到满足给定条件的索引,然后使用与这些索引对应的删除元素(元组)。以下是其代码。

indices = [i for i in range(len(somelist)) if(sum(somelist[i])==15)]newlist2 = [tup for j, tup in enumerate(somelist) if j not in indices]
print newlist2>>>[(1, 2, 3), (7, 8, 9), (10, 11, 12)]

方法1和方法2比方法3快。方法2和方法3比方法1更有效。I更喜欢方法2。对于上述示例,time(method1) : time(method2) : time(method3) = 1 : 1 : 1.7

如果您稍后将使用新列表,您可以简单地将elem设置为无,然后在稍后的循环中判断它,如下所示

for i in li:i = None
for elem in li:if elem is None:continue

通过这种方式,您不需要复制列表,并且更容易理解。

for循环将通过索引迭代。.

假设你有一个列表,

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

您使用名为lis的列表变量。并且您使用相同的来删除…

您的变量

lis = [5, 7, 13, 29, 35, 65, 91]0  1   2   3   4   5   6

在第五次迭代中,

您的第35不是质数,因此您将其从列表中删除。

lis.remove(y)

然后下一个值(65)移动到上一个索引。

lis = [5, 7, 13, 29, 65, 91]0  1   2   3   4   5

所以第4次迭代完成指针移动到第5…

这就是为什么你的循环没有覆盖65,因为它移动到上一个索引。

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

ite = lis #dont do it will reference instead copy

所以使用list[::]复制列表

现在你会给,

[5, 7, 13, 29]

问题是您在迭代期间从列表中删除了一个值,然后您的列表索引将崩溃。

所以你可以尝试理解。

它支持所有可迭代的,如列表、元组、字典、字符串等

如果您想在迭代时从列表中删除元素,请使用同时循环,以便您可以在每次删除后更改当前索引和结束索引。

示例:

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