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

Posted

技术标签:

【中文标题】为啥我可以在 Python for 循环中对迭代器和序列使用相同的名称?【英文标题】:Why can I use the same name for iterator and sequence in a Python for loop?为什么我可以在 Python for 循环中对迭代器和序列使用相同的名称? 【发布时间】:2014-09-01 14:54:00 【问题描述】:

这更像是一个概念性问题。我最近在 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 循环并使其按预期运行。它们在不同的范围内吗?允许这样的事情工作的引擎盖下发生了什么?

【问题讨论】:

作为一个有趣的思想实验:定义一个函数 printAndReturn,它接受一个参数,打印它,然后返回 is。那么在for i in printAndReturn [1,2,3,4,5] …中,[1,2,3,4,5]应该打印多少次? 关于范围的注释,因为没有其他人直接提到它:Python 具有函数级范围,但与 C 的块级范围不同。所以for循环的内部和外部具有相同的范围。 我更正了问题的标题,因为它有点误导。仅仅因为某事是不好的做法并不意味着它不起作用。可能只是它更容易出错,或者难以阅读/维护等。 谢谢。我完全同意这是一个糟糕的标题,我只是一开始不知道该取什么名字。 这在 php 中也可以工作 for ($x as $x) 但丑陋的代码 IMO 【参考方案1】:

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 永远不会再返回加载迭代器

除此之外:这样做没有任何意义,因为它已经有了迭代器,而Abhijit points out in his answer,Section 7.3 of Python's specification 实际上需要这种行为。

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

【讨论】:

“Python 永远不会再返回加载迭代器(这样做没有任何意义,因为它已经有了迭代器)。”这描述了您在反汇编中观察到的行为,但并没有说明 必须 是否是这种情况; Abhijit's answer 引用了实际指定的手册。【参考方案2】:

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

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

我希望您参考手册中的7.3. The for statement部分

摘录 1

表达式列表被计算一次;它应该产生一个可迭代的 目的。为 expression_list 的结果创建一个迭代器。

这意味着您的变量x,它是对象list 的符号名称:[1,2,3,4,5] 被评估为可迭代对象。即使变量、符号引用改变了它的忠诚度,因为 expression-list 不再被评估,对已经评估和生成的可迭代对象没有影响。

注意

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

摘录 2

然后,该套件针对由 迭代器,按索引升序排列。

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

【讨论】:

【参考方案3】:

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

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

我们无法在每次循环时查询序列,否则我们最终会第二次从 next 批次中读取 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];
    ...
 

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

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

【讨论】:

【参考方案4】:

这是变量 (x) 和它指向的对象(列表)之间的差异。当 for 循环开始时,Python 会获取对 x 指向的对象的内部引用。它使用对象,而不是 x 在任何给定时间发生的引用。

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

【讨论】:

【参考方案5】:

基本上,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
>>> 

在这个例子中,x 存储在foo() 中作为bar,所以虽然x 被重新分配,它仍然存在(ed)在foo() 中,所以我们可以使用它来触发我们的@ 987654331@循环。

【讨论】:

实际上,在最后一个示例中,我不认为 x 正在被重新分配。在foo 中创建了一个局部变量bar,并赋值为x。然后foo 以在for 条件中使用的对象的形式返回该值。因此,变量x 在第二个示例中从未被重新分配。不过我同意第一个。 @Tonio x 仍然是迭代变量,因此每个循环都采用一个新值。在循环之后,x 在两种情况下都等于3 @PeterGibson 你说得对,我没有注意到它。 如果它是循环中的一个“新变量”,那么循环之后x 持有3not [1,2,3]`怎么办? @JoshuaTaylor 在python中,循环索引变量的词法范围是发生for循环的块。【参考方案6】:

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

【讨论】:

它并没有复制范围列表(因为对列表的更改仍会在迭代中产生未定义的行为)。 x 只是停止引用范围列表,而是分配了新的迭代值。范围列表仍然完好无损。如果你在循环之后查看x的值,它将是4 "x 不再指代原来的 x" x 从未指代 x; x 引用了一个序列。然后它引用1,然后引用2,等等。

以上是关于为啥我可以在 Python for 循环中对迭代器和序列使用相同的名称?的主要内容,如果未能解决你的问题,请参考以下文章

Python:为啥这个 for 循环在第一次迭代后退出?

Python迭代器和生成器

《Python学习之路 -- Python基础之迭代器及for循环工作原理》

Python:替换多个 for 循环、多个迭代器

Python基础-----迭代器协议和For循环机制

是否可以在没有迭代器变量的情况下实现 Python for range 循环?