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

Why can I use the same name for iterator and sequence in a Python for loop?

这更像是一个概念性问题。我最近在python中看到了一段代码(它在2.7中工作,也可能在2.5中运行),其中一个for循环对正在迭代的列表和列表中的项使用了相同的名称,这对我来说既是糟糕的实践,也是不应该工作的东西。

例如:

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

产量:

1
2
3
4
5
6
1
2
3
4
5
5

现在,对我来说,最后一个打印的值是从循环中分配给x的最后一个值是有意义的,但是我不明白为什么您可以对for循环的两个部分使用相同的变量名,并使其按预期运行。它们在不同的范围内吗?引擎盖下面发生了什么让类似的东西工作?


dis告诉我们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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再也不会返回加载迭代器。

旁白:这样做没有任何意义,因为它已经有了迭代器,正如Abhijit在回答中指出的那样,Python规范的第7.3节实际上需要这种行为)。

当名称x被覆盖以指向列表中以前称为x的每个值时,python就不会遇到任何问题,因为它不需要再次查看名称x来完成迭代协议。


使用示例代码作为核心参考

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

我想请你参考第7.3节。手册中的for语句

节选1

The expression list is evaluated once; it should yield an iterable
object. An iterator is created for the result of the expression_list.

这意味着变量x,它是对象list的符号名:[1,2,3,4,5]被评估为一个不可重复的对象。即使变量、符号引用改变了它的效度,因为表达式列表不会被再次计算,也不会对已经被计算和生成的iterable对象产生影响。

注释

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

节选2

The suite is then executed once for each item provided by the
iterator, in the order of ascending indices.

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


如果你考虑的话,它必须这样工作。for循环序列的表达式可以是任何形式的:

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

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

Are they in different scopes?

否。要确认这一点,您可以保留对原始作用域字典(locals())的引用,并注意您实际上在循环中使用的是相同的变量:

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

What's going on under the hood that allows something like this to
work?

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

1
2
3
4
5
6
7
it = iter(x)
while True:
    try:
        x = it.next()
    except StopIteration:
        break
    print x

这与传统的索引方法不同,例如在旧版本的Java中可以看到:

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

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

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


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

如果重新分配X,则for循环不会更改。如果X指向可变对象(例如列表),而您更改了该对象(例如删除元素),则结果可能不可预测。


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

1
2
3
4
5
6
>>> x = [1, 2, 3]
>>> [x for x in x]
[1, 2, 3]
>>> x
3
>>>

就像这样:

1
2
3
4
5
6
7
8
9
10
11
>>> def foo(bar):
...     return bar
...
>>> x = [1, 2, 3]
>>> for x in foo(x):
...     print x
...
1
2
3
>>>

在本例中,x作为bar存储在foo()中,因此尽管x正在被重新分配,但它仍然存在于foo()中(ed),以便我们可以使用它来触发for循环。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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