枚举的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==3
、len(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]
现在被标记为x
和y
。 enumerate() 将继续使用y
标签,而不是x
标签。
del x[0]
存储在内存中的列表被修改,所以x
和y
现在都引用[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
您会注意到x
的id
每次循环都会发生变化,当您完成循环时,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奇怪行为的主要内容,如果未能解决你的问题,请参考以下文章