为啥我可以在 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
持有3
和not
[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 循环中对迭代器和序列使用相同的名称?的主要内容,如果未能解决你的问题,请参考以下文章