当你在迭代列表时,尝试删除其中一个元素会发生什么?

6
我正在按以下方式迭代列表:
some_list = [1, 2, 3, 4]
another_list = [1, 2, 3, 4]

for idx, item in enumerate(some_list):
    del some_list[idx]

for item in another_list:
    another_list.remove(item)

当我打印列表的内容时
>>> some_list
[2, 4]
>>> another_list
[2, 4]

我知道Python不支持在迭代列表时修改它,正确的方式是迭代列表的副本。但是我想了解背后的原理,即上面代码段的输出为什么是[2, 4]

1
你是否使用调试器或类似 http://pythontutor.com/visualize.html 的工具运行过这个程序? - jonrsharpe
2
等等,为什么这个问题被踩了?虽然迭代和删除的方法不太好,但输出结果让我很疑惑。 - Abdou
列表中的第一项被移除,然后列表向左移动,因此索引增加,并保留第二项。以此类推。 - Jean-François Fabre
1
看起来 Python 也有未定义行为,https://unspecified.wordpress.com/2009/02/12/thou-shalt-not-modify-a-list-during-iteration/。 - Jean-François Fabre
@thebjorn 有时即使使用可视化工具,对于初学者来说理解正在发生的事情也可能很困难。此外,这是那些支持在迭代过程中不删除列表项的论点更加有力的问题之一。 - Abdou
显示剩余5条评论
2个回答

12
你可以使用一个自制的迭代器来展示(在这种情况下使用print)迭代器的状态:
class CustomIterator(object):
    def __init__(self, seq):
        self.seq = seq
        self.idx = 0

    def __iter__(self):
        return self

    def __next__(self):
        print('give next element:', self.idx)
        for idx, item in enumerate(self.seq):
            if idx == self.idx:
                print(idx, '--->', item)
            else:
                print(idx, '    ', item)
        try:
            nxtitem = self.seq[self.idx]
        except IndexError:
            raise StopIteration
        self.idx += 1
        return nxtitem

    next = __next__  # py2 compat
然后将其用于你想要检查的列表周围:
some_list = [1, 2, 3, 4]

for idx, item in enumerate(CustomIterator(some_list)):
    del some_list[idx]

这应该说明了在那种情况下会发生什么:

give next element: 0
0 ---> 1
1      2
2      3
3      4
give next element: 1
0      2
1 ---> 3
2      4
give next element: 2
0      2
1      4

但它仅适用于序列。对于映射或集合,情况就更加复杂。


@thebjorn 是的,但是在迭代它们并改变它们(同时保持大小不变)可能会导致有趣的事情:例如 https://dev59.com/wFcO5IYBdhLWcg3wZQvX 和 https://stackoverflow.com/questions/45489688/can-anyone-explain-this-bizarre-bug-iterating-over-a-set。这更难以可视化(这就是我所指的),因为内部 set/dict 结构从 Python 内部不可见。 - MSeifert
1
这让我想起了Rick Cook的一句话(https://en.wikiquote.org/wiki/Rick_Cook) :-) - thebjorn

2

我希望了解幕后发生了什么。

我们知道,列表中的每个项目都有自己独特的索引;这些索引按顺序排列,从0开始。如果我们删除一个项目,则任何索引大于我们删除的索引的项目现在都已向下移动。

这就是为什么它很重要:

foo = ['a', 'b', 'c', 'd']
for index in range(len(foo)):
    del foo[index]

在这个循环中,我们正在删除所有元素,所以最终应该得到 foo == [],对吧?但实际情况并非如此。在第一次遍历中,我们删除了索引为0的项目,索引为1的项目成为了索引为0的项目。下一次遍历时,我们删除了索引为1的项目,它之前是索引为2的项目
仅在前两次迭代中,我们从数组中删除了'a''c',但我们忽略了删除'b'。当我们到达第三次迭代(我们认为我们将删除索引2)时,已经没有元素位于索引2了;只有索引01。当我们尝试删除不存在的索引2处的项目时,会引发异常,并停止循环。结果是一个混乱的数组,看起来像这样:['a', 'd']

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