为什么我可以在 Python for 循环中对迭代器和序列使用相同的名称?

这更多的是一个概念上的问题。我最近看到一段 Python 代码(它在2.7中工作,也可能在2.5中运行) ,其中一个 for循环对正在迭代的列表和列表中的项目使用相同的名称,这让我觉得既是不好的做法,也是根本不应该工作的。

例如:

x = [1,2,3,4,5]
for x in x:
print x
print x

收益率:

1
2
3
4
5
5

现在,对我来说,最后打印出来的值是循环中赋给 x 的最后一个值,这是有意义的,但是我不明白为什么您能够对 for循环的两个部分使用相同的变量名,并让它按预期的方式运行。他们在不同的范围内吗?引擎盖下到底发生了什么让这种东西成功运作?

5806 次浏览

基本上,for 循环接受列表 x,然后将其存储为临时变量,是的为该临时变量中的每个值分配一个 x。因此,x现在是列表中的最后一个值。

>>> x = [1, 2, 3]
>>> [x for x in x]
[1, 2, 3]
>>> x
3
>>>

就像这样:

>>> def foo(bar):
...     return bar
...
>>> x = [1, 2, 3]
>>> for x in foo(x):
...     print x
...
1
2
3
>>>

在这个例子中,xbar的形式存储在 foo()中,所以尽管 x被重新分配了,它仍然存在于 foo()中,因此我们可以使用它来触发 for循环。

x不再指原来的 x列表,因此没有混淆。基本上,python 记得它在迭代原始的 x列表,但是一旦开始将迭代值(0、1、2等)赋给名称 x,它就不再引用原始的 x列表。名称被重新分配给迭代值。

In [1]: x = range(5)


In [2]: x
Out[2]: [0, 1, 2, 3, 4]


In [3]: id(x)
Out[3]: 4371091680


In [4]: for x in x:
...:     print id(x), x
...:
140470424504688 0
140470424504664 1
140470424504640 2
140470424504616 3
140470424504592 4


In [5]: id(x)
Out[5]: 140470424504592

dis告诉我们什么:

Python 3.4.1 (default, May 19 2014, 13:10:29)
[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from dis import dis
>>> dis("""x = [1,2,3,4,5]
... for x in x:
...     print(x)
... print(x)""")


1           0 LOAD_CONST               0 (1)
3 LOAD_CONST               1 (2)
6 LOAD_CONST               2 (3)
9 LOAD_CONST               3 (4)
12 LOAD_CONST               4 (5)
15 BUILD_LIST               5
18 STORE_NAME               0 (x)


2          21 SETUP_LOOP              24 (to 48)
24 LOAD_NAME                0 (x)
27 GET_ITER
>>   28 FOR_ITER                16 (to 47)
31 STORE_NAME               0 (x)


3          34 LOAD_NAME                1 (print)
37 LOAD_NAME                0 (x)
40 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
43 POP_TOP
44 JUMP_ABSOLUTE           28
>>   47 POP_BLOCK


4     >>   48 LOAD_NAME                1 (print)
51 LOAD_NAME                0 (x)
54 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
57 POP_TOP
58 LOAD_CONST               5 (None)
61 RETURN_VALUE

关键位是第2和第3部分-我们从 x(24 LOAD_NAME 0 (x))加载值,然后得到它的迭代器(27 GET_ITER)并开始在它上面迭代(28 FOR_ITER)。Python 再也不用回去加载迭代器了.

除此之外: 这样做没有任何意义,因为它已经有了迭代器,而且作为 阿比吉特在他的回答中指出Python 规范的7.3部分实际上需要这种行为)。

当名称 x被覆盖以指向列表中以前称为 x的每个值时,Python 在寻找迭代器时没有任何问题,因为它不需要再次查看名称 x来完成迭代协议。

它是变量(x)和它指向的对象(列表)之间的区别。当 for 循环启动时,Python 获取对 x 指向的对象的内部引用。它使用对象,而不是 x 在任何给定时间引用的对象。

如果重新分配 x,for 循环不会改变。如果 x 指向一个可变对象(例如,一个列表) ,而你改变了该对象(例如,删除一个元素) ,结果可能是不可预测的。

使用示例代码作为核心引用

x = [1,2,3,4,5]
for x in x:
print x
print x

我希望你参考手册中的 7.3. for 语句部分

节选一

表达式列表计算一次; 它应该生成一个可迭代的 为 expression _ list 的结果创建一个迭代器。

这意味着变量 x(对象 list: [1,2,3,4,5]的符号名)被计算为一个可迭代对象。即使变量、符号引用改变了它的忠诚度,因为 表达式列表不会再次求值,也不会对已经求值和生成的可迭代对象产生影响。

注意

  • Python 中的一切都是一个对象,有一个标识符、属性和方法。
  • 变量是符号名,是对任何给定实例中一个且仅有一个对象的引用。
  • 运行时的变量可以更改其忠诚度,即可以引用其他一些对象。

节选二

对象提供的每个项执行一次套件 迭代器,按升序索引的顺序。

在这里,套件指的是迭代器,而不是表达式列表。因此,对于每个迭代,执行迭代器以生成下一个项,而不是引用原始表达式列表。

如果你仔细想想,这种方式是必要的。for循环序列的表达式可以是任何形式:

binaryfile = open("file", "rb")
for byte in binaryfile.read(5):
...

我们不能在每次通过循环时查询序列,否则在这里我们将第二次读取5个字节的 下一个批处理。当然,在循环开始之前,Python 必须以某种方式私下存储表达式的结果。


他们在不同的范围内吗?

没有。要确认这一点,您可以保留对原始范围字典(当地人)的引用,并注意到实际上在循环中使用了相同的变量:

x = [1,2,3,4,5]
loc = locals()
for x in x:
print locals() is loc  # True
print loc["x"]  # 1
break

引擎盖下面到底发生了什么 工作?

Sean Vieira 准确地展示了引擎盖下正在发生的事情,但是为了用更易读的 python 代码来描述它,你的 for循环实际上等同于这个 while循环:

it = iter(x)
while True:
try:
x = it.next()
except StopIteration:
break
print x

这不同于你在旧版 Java 中看到的传统的迭代索引方法,例如:

for (int index = 0; index < x.length; index++) {
x = x[index];
...
}

当项目变量和序列变量相同时,这种方法将失败,因为序列 x将不再可用于在第一次将 x重新分配给第一个项目后查找下一个索引。

然而,对于前一种方法,第一行(it = iter(x))请求 迭代器对象迭代器对象,而 迭代器对象迭代器对象实际上负责从那时起提供下一个项目。x最初指向的序列不再需要直接访问。