Python中enumerate函数的奇怪行为

19

我知道在循环中修改列表是不被允许的,但出于好奇,我想知道为什么以下两个示例的迭代次数会有所不同。

示例1:

x = [1, 2, 3, 4, 5]
for i, s in enumerate(x):
    del x[0]
    print(i, s, x)

例子2:

x = [1,2,3,4,5]
for i, s in enumerate(x):
    x = [1]
    print(i, s, x)

示例1仅运行3次,因为当i==3时,len(x)==2

示例2即使len(x)==1也会运行5次。

所以我的问题是,enumerate是在循环开始时生成完整的(index, value)对列表并遍历它吗?还是它们在每次循环迭代时生成?


5
我不会假装自己足够了解来回答你的问题,但是关于为什么行为不同,这是我的猜测。在第一种情况下,您正在从完全相同的列表中删除,因此迭代在之前停止是有道理的。然而,在第二种情况下,您正在重新分配变量。因此,Python可能将其视为“不同”的变量并继续使用x的“原始”值。 - bouletta
如果完全没有使用 enumerate,也会发生同样的情况!for 循环不会重新评估迭代器,所以即使你在 for 循环内部重新分配了 x,循环仍将使用旧值。显然,如果从列表中删除元素,则循环的迭代次数将减少。 - Bakuriu
1
这肯定是一个重复的内容。 - jpmc26
枚举需要适用于未绑定的序列,因此不可能提前生成这些对。如果您想要,只需使用 list(enumerate(...)) - John La Rooy
@jpmc26 确实有https://dev59.com/LXNA5IYBdhLWcg3wYMt-#986145,昨天我没有找到。它确实很好地解释了潜在的问题,但并不是在循环的上下文中。这应该被标记吗?我在SO上还很新,不太确定。 - Wisperwind
6个回答

22

在第一个示例中,实际上是在修改您正在迭代的列表。

另一方面,在第二种情况下,您只是将一个新对象分配给名称x。循环迭代的对象不会改变。

请查看http://foobarnbaz.com/2012/07/08/understanding-python-variables/,了解有关Python中名称和变量的详细说明。


谢谢你的回答!我之所以选择Wasi的答案,只是因为使用__next__()调用更清晰易懂。 - dbdq
@dbdq 我认为这更多与Python变量的工作方式有关,这可能是为什么这个答案有更多赞的原因。 - JeremiahBarrar
@dbdq 我认为我的回答已经很清楚地解释了这里发生的事情。 - JeremiahBarrar

14
enumerate() 返回一个迭代器,或者其他支持迭代的对象。enumerate() 返回的迭代器的 __next__() 方法返回一个元组,其中包含一个计数(从默认为0的起始点开始)和从 iterable 迭代获取的值。 __next__() 返回容器中的下一项。如果没有更多的项,则引发 StopIteration 异常。

enumerate() 是否在循环开始时生成完整的 (index, value) 对列表,并在每次循环迭代时遍历它?还是它们在每次循环迭代时生成?

所以,enumerate()返回一个迭代器,在每次迭代时,__next__()检查是否有更多的项。在循环开始时,enumerate()不会创建完整的列表。
正如@Wisperwind所提到的,在您的第二种情况中,您正在将一个新对象分配给名称x。循环迭代的对象在迭代过程中不会改变。

1
因此,即使将x=[0]赋值,原始的x值[1, 2,..., 5]仍不会被垃圾回收,因为迭代器仍在引用此列表。 - dbdq

8

对Wasi Ahmad和Wisperwind所说的内容进行澄清。他们都表示“你只是将一个新对象赋给了名称x”。这可能会有点令人困惑,因为它可能被解释为“你正在创建一个新对象([1])并将其存储到名称x中,你可能会说:“好吧,那为什么它没有改变?!”要查看发生了什么,请打印对象的id。

x = [1, 2, 3, 4, 5]
y = x  # To keep a reference to the original list
print id(x), id(y)
for i, v in enumerate(x):
    x = [1]
    print id(x), id(y)
print id(x), id(y)


# output (somewhat contrived as I don't have a python environment set up)
#    X ID            Y ID
10000000000001 10000000000001
10000000000002 10000000000001
10000000000003 10000000000001
10000000000004 10000000000001
10000000000005 10000000000001
10000000000006 10000000000001
10000000000006 10000000000001

您会注意到,在循环中每次都会更改xid,并且在循环结束时,x将指向循环中进行的最后一次修改。当您通过循环时,它正在迭代原始实例的x,无论您是否仍然可以引用它。
正如您所看到的,y指向原始的x。当您通过循环进行迭代时,即使x正在更改,y仍然指向仍在循环中被遍历的原始x

Python是一种非常基于引用的语言。你不会给变量名x赋值,而是给变量名x赋一个引用。 - Penguin Brian
当我运行这段代码时,循环内部的x的id在139917134004304和139917134053248之间交替变化。这是因为新对象被创建在与上一个对象相同的位置。然而,这个问题会让你的答案变得更加复杂 - Martin Bonner supports Monica
此外,如果 OP 想要减少 x 引用的原始列表(他正在循环遍历),他可以编写:del x[1:] : x[0] = 1 - Martin Bonner supports Monica
1
@MartinBonner 或者甚至可以这样写:x[:] = [1] - Jasmijn

2

其他人已经指出,你的第二个例子只改变了 x所指向的值,但没有改变你正在迭代的列表。这是普通赋值(x = [1])和切片赋值 (x[:] = [1])之间差异的完美例子。后者直接在原地修改了 x 所指向的列表:

x = [1, 2, 3, 4, 5]
for i, s in enumerate(x):
    x[:] = [1]
    print(i, s, x)

将会打印

(0, 1, [1])

1

确实:你的第一个片段直接在原列表上进行修改;第二个片段将变量x指向一个新的列表,未修改由enumerate()遍历的列表。您可以通过访问www.pythontutor.com上的以下链接来查看其效果,这些链接允许您逐步执行代码并可视化变量的内容:

为了更好地了解正在发生的情况,请转到这里,而不是跨越以下扩展代码的步骤。
x = [1,2,3,4,5]
view = enumerate(x)
for i, s in view:
    x = [1]
    print(i, s, x)

0
x = [1, 2, 3, 4, 5]

列表[1, 2, 3, 4, 5]被标记为x
for i, s in enumerate(x):

enumerate()函数会附加另一个标签,所以[1, 2, 3, 4, 5]现在被标记为xy。enumerate()函数将继续使用y标签,而不是x标签。

del x[0]

存储在内存中的列表已被修改,因此xy现在都引用[2, 3, 4, 5]

或者,当您使用

x = [1]

在内存中创建了一个新的列表[1],并且x标签现在指向该列表。而y标签仍然指向原始列表。
Python变量的工作原理:
http://foobarnbaz.com/2012/07/08/understanding-python-variables/

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