【问题标题】:Should I create a copy when iterating over a list using enumerate in Python在 Python 中使用 enumerate 遍历列表时是否应该创建副本
【发布时间】:2018-07-30 19:45:55
【问题描述】:

在回复this question 时,我遇到了一些我在 Python 中从未想过的事情(由用户指出)。

基本上,我已经知道(这里有一个有趣的thread),我必须在 Python 中对列表进行变异时进行迭代,以避免出现奇怪的行为。

现在,我的问题是,使用 enumerate 是否可以解决这个问题?

test_list = [1,2,3,4]
for index,item in enumerate(test_list):
    if item == 1:
        test_list.pop(index)

这段代码会被认为是安全的还是我应该使用,

for index,item in enumerate(test_list[:]):

【问题讨论】:

  • 但是什么会更安全呢?无论如何,您正在修改原始列表,对吗?只要您在循环期间不从列表中删除项目,就可以不创建列表的副本。
  • @Shan 我想我一直都错了。我认为在迭代列表时修改列表在 python 中是被禁止的,并且可能导致奇怪的输出。为了记录,我认为我的第二个例子会更安全。
  • 改变列表的大小或项目排序可能会导致意外行为。更改单个项目的值可能是无害的。
  • @scharette 复制总是安全的——除非分配一个新列表并复制所有元素会导致MemoryError 或者花费很长时间以至于您的算法不再有用,当然。问题是不复制是否“足够安全”,这是一个比它应该的更棘手的问题。
  • 是的,我明白了。我的问题实际上是关于改变一个列表。为了这个问题,我创建了一个过于简单的例子。例如,我是否应该更改 test_list.pop(index) 的声明。那会更合适。

标签: python list iteration


【解决方案1】:

首先,让我们回答您的直接问题:

enumerate 在这里没有任何帮助。它的工作方式就好像它拥有一个指向底层可迭代对象的迭代器(并且,至少在 CPython 中,这正是它所做的),所以任何对列表迭代器不合法或不安全的事情都是不合法或不安全的使用围绕该列表迭代器的枚举对象。


您的原始用例——设置test_list[index] = new_value——在实践中是安全的——但我不确定它是否保证安全。

您的新用例(调用 test_list.pop(index))可能不安全。


列表迭代器最明显的实现基本上只是对列表的引用和对该列表的索引。因此,如果您在当前位置或该位置的左侧插入或删除,您肯定会破坏迭代器。例如,如果您删除 lst[i],这会将所有内容从 i + 1 移动到最后一个位置,因此当您继续移动到 i + 1 时,您将跳过原始的 i + 1th 值,因为它现在是ith。但是如果你在当前位置的右边插入或删除,那就没问题了。

由于test_list.pop(index) 在当前位置或左侧删除,因此即使使用此实现也不安全。 (当然,如果您已经仔细编写了算法,以便在命中后跳过该值无关紧要,也许这也没关系。但更多的算法无法处理。)

可以想象,Python 实现可以存储一个原始指针,指向用于列表存储的数组中的当前位置。这意味着插入 anywhere 可能会破坏迭代器,因为插入会导致整个列表重新分配到新内存。如果实现有时会在缩小时重新分配列表,那么可以在任何地方删除。我不认为 Python 不允许执行所有这些操作,因此如果您想偏执,在迭代时从不插入或删除可能会更安全。

如果您只是替换现有值,很难想象在任何合理的实现下这会如何破坏迭代器。但是,据我所知,语言参考和list 库参考1 实际上并没有对列表迭代器的实现做出任何承诺。2

因此,您是否关心“我的实现安全”、“每个迄今为止编写的实现都安全”、“每个可能的(对我)实现的安全”还是“由参考保证安全”取决于您”。

我认为大多数人在迭代过程中很乐意替换列表项,但要避免缩小或扩大列表。但是,肯定有生产代码至少删除了迭代器的右侧。


1.我相信 tutorial 只是在某处说在迭代时永远不要修改任何数据结构——但这就是教程。始终遵循该规则当然是安全的,但遵循不太严格的规则也可能是安全的。

2。除非key 函数或其他任何东西试图在sort 中间以任何方式访问列表,结果是未定义的。

【讨论】:

  • 已经投票了。不过,您知道,我编辑是为了现在改变列表,如果这个问题在上下文中更合乎逻辑。
  • @scharette 好的,已编辑。即使在实践中,您所做的事情也可能不安全。
【解决方案2】:

由于是我的comment 导致了这一点,我将添加我的后续:

enumerate 可以被认为是一个生成器,所以它只需要一个序列(实际上是任何序列或迭代器),并且只是“生成”一个递增索引,以便从传递的序列中产生每个项目(所以它不是复制或者只是使用"enumerate object" 来改变列表)。

在该问题中的代码的情况下,您永远不会更改您正在迭代的列表的长度,并且一旦运行 if 语句,元素的值就无关紧要了。因此不需要副本,当删除元素时需要它,因为迭代器索引与列表共享并且不考虑删除的元素。

The Python Ninja 有一个很好的例子,说明何时应该使用副本(或转向列表理解)

【讨论】:

  • 你说得对,这是一个糟糕的用例。我实际上编辑了我的答案,现在改变了长度。从逻辑上讲,这就是我想知道的。
  • 是的,这就是为什么我说“你没有改变长度”:)。如果您改变长度,那么在没有副本的情况下使用枚举不是一个好习惯。即便如此,我实际上可能不会在实践中这样做,而是转向列表理解或自定义生成器以避免丢失或不正确的数据(或错误)。有时间我会更直接地编辑它。
  • 我明白,我投了赞成票,因为对于我过去的例子,你是完全正确的。
猜你喜欢
  • 2014-05-25
  • 2015-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-10
  • 1970-01-01
  • 1970-01-01
  • 2020-08-12
相关资源
最近更新 更多