枚举的Python奇怪行为

Posted

技术标签:

【中文标题】枚举的Python奇怪行为【英文标题】:Python strange behavior with enumerate 【发布时间】:2017-07-28 00:38:37 【问题描述】:

我知道我不应该在循环中修改列表,但出于好奇,我想知道为什么以下两个示例的迭代次数不同。

示例 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==3len(x)==2 时。

示例 2 运行 5 次,即使 len(x)==1

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

【问题讨论】:

我不能假装我知道的足以回答你的问题,但至于为什么它的行为不同,这是我的猜测。在第一种情况下,您将从同一个列表中删除,因此迭代之前停止是有意义的。但是,在第二种情况下,您正在重新分配它。所以python可能认为它是一个different变量并继续使用x的original值。 完全没有枚举也会发生同样的情况! for 循环不会重新评估迭代器,因此即使您在 for 循环内重新分配 x,循环仍将使用旧值。显然,如果您从列表中删除元素,循环将以更少的迭代完成。 这一定是骗人的。 enumerate 需要适用于未绑定的序列,因此无法预先生成对。如果你想,只需使用list(enumerate(...)) @jpmc26 确实有 ***.com/a/986145/3451198 我昨天没有找到。它确实很好地解释了潜在问题,但不是在循环的上下文中。这应该被标记吗?我是 SO 的新手,不确定。 【参考方案1】:
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/

【讨论】:

【参考方案2】:

其他人已经指出,您的第二个示例仅更改了 x 指向的值,而不是您正在迭代的列表。这是普通赋值 (x = [1]) 和 slice assignment (x[:] = [1]) 之间区别的完美示例。后者修改列表x指向就地

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

将打印

(0, 1, [1])

【讨论】:

【参考方案3】:

确实:您的第一个 sn-p 修改了迭代列表;第二个将变量x 指向一个新列表,使enumerate() 横向的列表保持不变。您可以通过访问 www.pythontutor.com 上的以下链接来查看实际情况,这使您可以单步执行代码并可视化变量的内容:

First version(x 已就地修改)。

Second version(x 被重定向到[1])。

为了更好地了解发生了什么,请转到here,而不是跳过以下扩展代码:

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

【讨论】:

【参考方案4】:

enumerate() 返回一个迭代器,或其他支持迭代的对象。 enumerate() 返回的迭代器的 __next__() 方法返回一个元组,其中包含一个计数(从 start 默认为 0)和通过迭代 iterable 获得的值。

__next__() 从容器中返回下一项。如果没有其他项目,请引发 StopIteration 异常。

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

所以,enumerate() 返回一个迭代器,并且在每次迭代时,__next__() 检查是否还有其他项目。 enumerate() 不会在循环开始时创建完整列表。

正如@Wisperwind 所提到的,在您的第二种情况下,您将一个新对象分配给名称x。循环迭代的对象在迭代期间不会改变。

【讨论】:

所以原 x 的值 [1, 2,..., 5] 即使在分配 x=[0] 后也没有被垃圾回收,是因为迭代器仍然引用这个列表.【参考方案5】:

只是对 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,而该 x 仍在循环中。

【讨论】:

Python 在很大程度上是一种基于参考的语言。您没有为名称 x 赋值,而是为名称 x 分配了一个引用。 当我运行此代码时,循环内x 的 ID 在 139917134004304 和 139917134053248 之间交替。这是因为新对象与最后一个对象创建在同一位置.但是,这种皱纹会使您的答案更加复杂! 另外,如果 OP 想要减少 x 引用的 original 列表(并且他正在循环),他可以写:del x[1:] : x[0] = 1跨度> @MartinBonner 甚至:x[:] = [1]【参考方案6】:

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

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

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

【讨论】:

感谢您的回答!我选择 Wasi 的答案只是因为使用 __next__() 调用更容易理解。 @dbdq 我认为这与 Python 变量的工作方式有关,这可能就是为什么这个答案有更多的支持。 @dbdq 我认为我的回答很清楚这里发生了什么

以上是关于枚举的Python奇怪行为的主要内容,如果未能解决你的问题,请参考以下文章

python(pytz)中的奇怪时区行为

奇怪的 Python 日期比较行为

“__”函数的Python奇怪的覆盖行为

Python数组奇怪的行为? [复制]

python“或”运算符奇怪的行为

在 Python 中为 True 定义值时的奇怪行为