我在Python中迭代一个元组列表,并试图删除它们,如果它们满足某些条件。
for tup in somelist:
if determine(tup):
code_to_remove_tup
我应该用什么来代替code_to_remove_tup?我不知道怎么用这种方式把东西拿掉。
我在Python中迭代一个元组列表,并试图删除它们,如果它们满足某些条件。
for tup in somelist:
if determine(tup):
code_to_remove_tup
我应该用什么来代替code_to_remove_tup?我不知道怎么用这种方式把东西拿掉。
当前回答
你可以反过来尝试for- loops,这样对于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)
这样索引是对齐的,并且不会受到列表更新的影响(无论是否弹出cur元素)。
其他回答
如果稍后将使用新列表,可以简单地将elem设置为None,然后在后面的循环中判断它,如下所示
for i in li:
i = None
for elem in li:
if elem is None:
continue
这样,你就不需要复制列表,而且更容易理解。
for i in range(len(somelist) - 1, -1, -1):
if some_condition(somelist, i):
del somelist[i]
你需要向后走,否则就有点像锯掉你坐着的树枝:-)
Python 2用户:用xrange替换range以避免创建硬编码的列表
TLDR:
我写了一个库,允许你这样做:
from fluidIter import FluidIterable
fSomeList = FluidIterable(someList)
for tup in fSomeList:
if determine(tup):
# remove 'tup' without "breaking" the iteration
fSomeList.remove(tup)
# tup has also been removed from 'someList'
# as well as 'fSomeList'
如果可能的话,最好使用另一种方法,在迭代时不需要修改你的iterable,但对于某些算法来说,它可能没有那么直接。所以如果你确定你真的想要原始问题中描述的代码模式,这是可能的。
应该适用于所有可变序列,而不仅仅是列表。
完整的回答:
编辑:这个回答中的最后一个代码示例给出了一个用例,说明为什么有时希望就地修改列表,而不是使用列表理解式。答案的第一部分是关于如何适当修改数组的教程。
解决方案从senderle的回答(一个相关的问题)开始。它解释了如何在遍历已修改的列表时更新数组索引。下面的解决方案旨在正确跟踪数组索引,即使列表被修改。
从这里下载fluidIter.py https://github.com/alanbacon/FluidIterator,它只是一个单独的文件,所以不需要安装git。没有安装程序,所以你需要确保文件在你自己的python路径下。该代码是为python 3编写的,尚未在python 2上进行测试。
from fluidIter import FluidIterable
l = [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: 0
popped value: 2
initial state of list on this iteration: [0, 1, 3, 4, 5, 6, 7, 8]
current iteration value: 1
popped value: 3
initial state of list on this iteration: [0, 1, 4, 5, 6, 7, 8]
current iteration value: 4
popped value: 4
initial state of list on this iteration: [0, 1, 5, 6, 7, 8]
current iteration value: 5
popped value: 5
initial state of list on this iteration: [0, 1, 6, 7, 8]
current iteration value: 6
popped value: 6
initial state of list on this iteration: [0, 1, 7, 8]
current iteration value: 7
popped value: 7
initial state of list on this iteration: [0, 1, 8]
current iteration value: 8
popped value: 8
Final List Value: [0, 1]
上面我们已经在流体列表对象上使用了pop方法。还实现了其他常见的可迭代方法,如del fluidL[i], .remove, .insert, .append, .extend。还可以使用切片修改列表(不实现排序和反向方法)。
唯一的条件是您必须只修改列表,如果在任何时候fluidL或l被重新分配给不同的列表对象,代码将无法工作。原始的fluidL对象仍将被for循环使用,但将超出我们修改的范围。
i.e.
fluidL[2] = 'a' # is OK
fluidL = [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 index
fluidArrIter = 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: 0
current val: 0
current ind: 0
[0, 1, 2, 3]
enum: 1
current val: 1
current ind: 2
['a', 0, 1, 2, 3]
enum: 2
current val: 2
current ind: 4
['a', 'a', 0, 1, 2, 3]
enum: 3
current val: 3
current 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回答中提到的列表理解。话虽如此,我发现这个类对我来说很有用,而且比跟踪需要删除的元素的索引更容易使用。
编辑:正如评论中提到的,这个答案并没有真正提出这个方法提供解决方案的问题。我将在这里尝试解决这个问题:
列表推导式提供了一种生成新列表的方法,但这些方法倾向于孤立地查看每个元素,而不是将列表的当前状态作为一个整体。
i.e.
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 itself
if 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 loop
del 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: 20
remove val: 80
outer val: 61
outer val: 54
outer val: 18
remove val: 54
remove val: 18
outer val: 7
remove val: 70
outer val: 55
outer val: 9
remove val: 18
final list: [20, 61, 7, 55, 9]
其他答案是正确的,从你正在迭代的列表中删除通常是一个坏主意。反向迭代避免了一些陷阱,但是要遵循这样做的代码要困难得多,所以通常您最好使用列表理解或过滤器。
然而,有一种情况下,从您正在迭代的序列中删除元素是安全的:如果您在迭代时只删除一项。这可以通过返回或中断来确保。例如:
for i, item in enumerate(lst):
if item % 4 == 0:
foo(item)
del lst[i]
break
当您对满足某些条件的列表中的第一个项执行一些具有副作用的操作,然后立即从列表中删除该项时,这通常比列表推导式更容易理解。
这个答案最初是为了回答一个问题而写的,这个问题后来被标记为重复: 在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)
第一个问题是同时向remove()传递'a'和'b',但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”来验证这一点,它不同于仅仅的“equals”(==)。
>>> 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'对象。出于实际目的,您可以将其视为创建其参数的反向副本。这是我推荐的解决方案。