在Python中迭代一个列表并移除其中的某个元素时,结果可能会非常奇怪。

100
我有这段代码:
numbers = list(range(1, 50))

for i in numbers:
    if i < 20:
        numbers.remove(i)

print(numbers)

但是,我得到的结果是:
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]

当然,我期望结果中不会出现小于20的数字。看起来我在remove操作方面做错了什么。


另请参阅:https://dev59.com/p3M_5IYBdhLWcg3w1G6N。我重新考虑并决定这不是重复的问题;这个问题是关于理解一种错误方法失败的原因,而另一个问题则涉及寻找正确方法。 - Karl Knechtel
12个回答

149
你在迭代列表的同时修改了它。这意味着第一次循环时,i等于1,所以1被从列表中移除。然后for循环进入列表的第二个项目,不是2,而是3!然后将其从列表中移除,然后for循环继续到列表的第三个项目,现在是5。依此类推。也许用一个^指向i的值更容易理解:
[1, 2, 3, 4, 5, 6...]
 ^

这是列表的初始状态;然后移除1,循环进入列表的第二个项目:

[2, 3, 4, 5, 6...]
    ^
[2, 4, 5, 6...]
       ^

等等。

在迭代列表时,没有好的方法可以改变列表的长度。你所能做的最好的事情就是像这样:

numbers = [n for n in numbers if n >= 20]

如果需要原地修改,则可以使用以下代码(括号中的部分是生成器表达式,在进行切片赋值之前会自动转换为元组):

numbers[:] = (n for n in numbers if n >= 20)

如果您想在删除 n 之前对其执行某个操作,可以尝试以下技巧:
for i, n in enumerate(numbers):
    if n < 20 :
        print("do something")
        numbers[i] = None
numbers = [n for n in numbers if n is not None]
    

2
Python文档中关于for循环保留索引的相关说明 https://docs.python.org/3.9/reference/compound_stmts.html#the-for-statement: “当循环修改序列时(仅适用于可变序列,例如列表),存在一个微妙之处。内部计数器用于跟踪下一个使用的项目,并在每次迭代时递增。...这意味着如果代码块从序列中删除当前(或先前)项目,则下一个项目将被跳过(因为它获取已经处理过的当前项目的索引)。” - Gino Mempin
1
这是一个很好的答案,但最终解决方案中,“如果你想执行一个操作...”略微不尽人意,因为1)实际上没有必要包含那个限定语:在单个操作中迭代时尝试删除元素只是一种浪费精力的行为,因此这个两阶段的解决方案适用于所有情况;2)因为应该警告设置为None并不总是合适(如果某些元素本来就是 None),需要一些传统的“毒丸”值,适合于手头的情况。 - mike rodent
当我提到“努力的浪费”时,我指的是“通用”的解决方案,即在需要删除随机元素时,而不是“专业”的情况,例如仅在开头(或仅在结尾)删除元素,这种情况更适合于一些简单的方法,比如你的列表推导式解决方案。 - mike rodent

24

从列表末尾开始向后遍历:

li = list(range(1, 15))
print(li)

for i in range(len(li) - 1, -1, -1):
    if li[i] < 6:
        del li[i]
        
print(li)

结果:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] 
[6, 7, 8, 9, 10, 11, 12, 13, 14]

3
我多么希望我能给这个答案点赞 +2!优雅,简单...不完全混淆。 - David Hempy
1
这是一个非常专业化的答案:实际上并不清楚我们是否应该寻找如何在迭代时删除元素的通用解决方案,还是仅在我们想要删除列表的前n个元素时如何独占地执行此操作。所选答案提供了前者,这更加有帮助,但也提供了后者,即一行列表推导式解决方案。 - mike rodent
2
@mikerodent 不是这样的。当你想在迭代列表时修改它时,倒序遍历是非常常见的做法。 - user3064538
1
@Boris,你没有理解我的评论。OP的问题并没有指定我们要删除连续的元素(无论是从列表的开头还是结尾)。 - mike rodent
2
我还是不理解你的评论,因为无论列表是洗牌还是有序,这段代码仍然可以工作。 - user3064538
这就是我的直觉带我来到的地方。你还需要迭代的部分不会受到任何影响! - undefined

11

@senderle的回答是正确的!

话虽如此,为了更好地阐述你的问题,如果你考虑一下,你总是想要删除索引0 20次:

[1,2,3,4,5............50]
 ^
[2,3,4,5............50]
 ^
[3,4,5............50]
 ^

所以你可以使用类似这样的代码:

aList = list(range(50))
i = 0
while i < 20:
    aList.pop(0)
    i += 1

print(aList) #[21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]

希望这有所帮助。


据我所知,以下方法并不算是坏实践。

编辑(更多内容):

lis = range(50)
lis = lis[20:]

同样可以完成任务。

编辑2(我无聊了):

functional = filter(lambda x: x> 20, range(50))

3

我找到了一个解决方案,但是它非常笨拙...

首先你需要创建一个索引数组,其中列出你想要删除的所有索引,就像以下示例一样

numbers = range(1, 50)
index_arr = []

for i in range(len(numbers):
    if numbers[i] < 20:
        index_arr.append(i)

接下来,您想要删除在index_arr中保存的索引对应的numbers列表中所有的条目。您将遇到与之前相同的问题。因此,在从numbers arr中删除一个数字后,您需要从index_arr中的每个索引中减去1,如下所示:

numbers = range(1, 50)
index_arr = []

for i in range(len(numbers):
    if numbers[i] < 20:
        index_arr.append(i)

for del_index in index_list:
    numbers.pop(del_index)

    #the nasty part
    for i in range(len(index_list)):
        index_list[i] -= 1

这个方法可以实现,但我猜这不是预期的做法


2
作为对@Senderle答案的补充信息,仅供记录,我认为将Python在“序列类型”上看到for的逻辑可视化会很有帮助。
假设我们有:
lst = [1, 2, 3, 4, 5]

for i in lst:
    print(i ** 2)

这实际上将是:

index = 0
while True:
    try:
        i = lst.__getitem__(index)
    except IndexError:
        break
    print(i ** 2)
    index += 1

这是一个try-catch机制,当我们在序列类型或可迭代对象上使用for时会出现这种情况(尽管有些不同 - 调用next()StopIteration异常)。
我想说的是,Python会跟踪一个独立的变量,称为index,因此无论列表发生什么变化(删除或添加),Python都会增加该变量并使用“该变量”调用__getitem__()方法,并请求项目。

1

在@eyquem的回答基础上进行简化和扩展...

问题是当你迭代时,元素被从中间移除,导致你跳过了一些数字。

如果你从末尾开始并向后移动,在进行删除操作时不会有影响,因为当它移到“下一个”项目(实际上是前一个项目)时,删除不会影响列表的前半部分。

只需在迭代器中添加reversed()即可解决该问题。最好加上注释,以防未来的开发人员“整理”你的代码并神秘地破坏它。

for i in reversed(numbers): # `reversed` so removing doesn't foobar iteration
  if i < 20:
    numbers.remove(i)

0

您可以使用list()来处理数字,从而创建如下所示的不同副本数字

numbers = list(range(1, 50))
       # ↓ ↓ Here ↓ ↓
for i in list(numbers):
    if i < 20:
        numbers.remove(i)

print(numbers) # [20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 
               #  31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 
               #  f42, 43, 44, 45, 46, 47, 48, 49]

0
自从 Python 3.3 版本以来,您可以使用列表 copy() 方法作为迭代器:
numbers = list(range(1, 50))

for i in numbers.copy():
    if i < 20:
        numbers.remove(i)
print(numbers)

[20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]

这似乎是对一个经典问题的巧妙建议,但我不确定实际上它是否比所选答案末尾建议的两阶段解决方案更好:首先,copy操作的成本是多少?其次,您必须关注这是深拷贝还是浅拷贝。在这个微不足道的例子中,这个问题并不会出现,但并非总是如此。 - mike rodent

0

你也可以使用 continue 来 忽略小于20的值

mylist = []

for i in range(51):
    if i<20:
        continue
    else:
        mylist.append(i)
print(mylist)

0

对于迭代,制作numbers的浅拷贝将确保用于迭代的列表不会被修改。可以使用list()copy.copy()进行浅拷贝,并确保要删除的元素的标识相同,并减小临时列表的大小。 代码:

...
for i in list(numbers):
    if i < 20:
        numbers.remove(i)
...

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