我在Python中迭代一个元组列表,并试图删除它们,如果它们满足某些条件。

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

我应该用什么来代替code_to_remove_tup?我不知道怎么用这种方式把东西拿掉。


当前回答

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]

其他回答

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

for i in li:
    i = None

for elem in li:
    if elem is None:
        continue

这样,你就不需要复制列表,而且更容易理解。

您需要获取列表的副本并首先对其进行迭代,否则迭代将失败,可能会出现意想不到的结果。

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

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 = [x for x in somelist if not determine(x)]

或者,通过分配slice somlist[:],你可以改变现有的列表,只包含你想要的项:

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

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

你也可以使用itertools来代替理解函数。在Python 2中:

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

或者在python3中:

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

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

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

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

然而,有一种情况下,从您正在迭代的序列中删除元素是安全的:如果您在迭代时只删除一项。这可以通过返回或中断来确保。例如:

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

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