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


78

这更多是一个概念上的问题。最近,我在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循环的两个部分中使用相同的变量名并具有它按预期起作用。它们在不同的范围内吗?允许这种事情发生的事情到底是怎么回事?


1
作为一个有趣的思想实验:定义一个函数printAndReturn,该函数接受一个参数,将其打印,然后返回is。然后在中for i in printAndReturn [1,2,3,4,5] …,应该[1,2,3,4,5]打印多少次?
约书亚·泰勒

1
关于作用域的说明,因为没有人直接提及它:Python具有函数级作用域,但没有C的块级作用域。因此,for循环的内部和外部具有相同的范围。
Izkata 2014年

我更正了问题的标题,这有点令人误解。仅仅因为某种不好的做法,并不意味着它就行不通。这可能只是它更容易出错,或难以阅读/保持等
尼科

谢谢。我完全同意这是一个不好的称呼,只是我最初不知道该用什么来命名。
古斯塔夫

这在php中也有效,for ($x as $x)但是代码丑陋,IMO
chiliNUT

Answers:


67

什么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部分-我们从x24 LOAD_NAME 0 (x))中加载值,然后获得其迭代器(27 GET_ITER)并开始对其进行迭代(28 FOR_ITER)。Python再也不会返回以再次加载迭代器

旁白:这将没有任何意义的话,因为它已经拥有了迭代器,并作为作者Abhijit在他的回答指出Python的规范的第7.3实际上需要这种行为)。

当名称x被覆盖以指向列表中以前称为xPython的每个值时,找到迭代器不会有任何问题,因为它无需x再次查看名称即可完成迭代协议。


8
“ Python再也不会回去再次加载迭代器(这样做已经没有意义了,因为它已经有了迭代器)。” 这说明你在观察拆卸,但它并没有说是否该行为是事实与否; Abhijit的答案引用了实际指定的手册。
约书亚·泰勒

42

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

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

我希望您参考第7.3手册中的for声明

摘录1

表达式列表被评估一次;它应该产生一个可迭代的对象。为expression_list的结果创建一个迭代器。

它的意思是,你的变量x,它是对象的符号名list[1,2,3,4,5]被评估为一个迭代的对象。即使变量,符号引用更改了其忠诚度,因为不再对表达式列表进行求值,对已经求值和生成的可迭代对象也没有影响。

注意

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

摘录2

然后,对于该迭代器提供的每个项目,按升序对索引执行一次套件。

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


5

如果您考虑一下,则必须以这种方式工作。for循环序列的表达式可以是任何东西:

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

我们无法在循环的每次遍历中查询序列,否则在这里我们将第二次从一批5字节中读取数据。自然,Python必须以某种方式在循环开始之前私下存储表达式的结果。


它们在不同的范围内吗?

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

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];
    ...
 }

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

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


4

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

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


3

基本上,for循环接收list x,然后将其存储为临时变量,然后为该临时变量中的每个值重新分配a 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
>>> 

在此示例中,x存储为foo()as bar,因此尽管x被重新分配了,但它仍存在于其中,foo()因此我们可以使用它来触发for循环。


实际上,在最后一个示例中,我认为x没有被重新分配。在bar中创建局部变量foo并为其指定值xfoo然后以该for条件中使用的对象的形式返回该值。因此,x在第二个示例中,从未重新分配变量。我同意第一个。
Tonio 2014年

@Toniox仍然是迭代变量,因此为每个循环取一个新值。循环之后,在两种情况下x均等于3
彼得·吉布森

@PeterGibson你说的很对,它超出了我的注意力。
Tonio 2014年

如果它是一个“新的变量”的循环中,那么如何来循环后x持有3not [1,2,3]`?
约书亚·泰勒

@JoshuaTaylor在python中,循环索引变量的词法范围仅限于发生for循环的块。
HennyH 2014年

1

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

2
它并不会复制范围列表(因为列表的更改仍会在迭代中产生未定义的行为)。x只是停止引用范围列表,而是分配了新的迭代值。范围列表仍然完整存在。如果看一下x循环后的值,它将是4
Peter Gibson

x从未提及的“ x不再是原始的x” xx指一个序列。然后,它被称为1,然后向2
约书亚泰勒
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.