为什么分配给多个目标(标识符/属性)会产生奇怪的结果? [英] Why is assigning to multiple targets (identifier/attribute) producing strange results?
问题描述
我有一些这样的代码:
def foo():bar = initial_bar = Bar()为真:next_bar = Bar()bar.next_bar = next_barbar = next_bar返回 initial_bar
目的是形成一个Bar
链,可以遵循,链表样式.
这一切都很好;但由于一些误导性的想法,我想将其减少一行,将循环末尾的分配组合成一行.
def foo():bar = initial_bar = Bar()为真:next_bar = Bar()bar = bar.next_bar = next_bar返回 initial_bar
因为 bar = bar.next_bar = next_bar
将扩展为 bar.next_bar = next_bar
后跟有效的 bar = bar.next_bar
.(除非它没有.)
问题是,这行不通;返回的初始栏"没有定义它的 next_bar
.通过返回到更明确的两行解决方案,我可以很容易地解决这个问题,但是发生了什么?
是时候退出 dis
.
如果你仔细观察,你会看到在关键行(第 5 行,看到左边的数字,位置 31-47),它是这样做的:
- 加载
next_bar
(31) 两次 (34); - 将它(堆栈中的第一个副本)写入
bar
(35); - 将它(堆栈中的第二个副本)写入
bar.next_bar
(38, 41).
这在最小测试用例中更明显.
<预><代码>>>>定义一个():... b = c = d...>>>dis.dis(a)2 0 LOAD_GLOBAL 0 (d)3 DUP_TOP4 STORE_FAST 0 (b)7 STORE_FAST 1 (c)10 LOAD_CONST 0(无)13 RETURN_VALUE看看它在做什么.这意味着 b = c = d
实际上等价于 b = d;c = d
.通常这无关紧要,但在最初提到的情况下,这确实很重要.这意味着在临界线上,
bar = bar.next_bar = next_bar
不等于
bar.next_bar = next_barbar = next_bar
而是要
bar = next_barbar.next_bar = next_bar
事实上,这在 Python 文档的第 6.2 节中有记录,简单语句、赋值语句:
<块引用>赋值语句计算表达式列表(请记住,这可以是单个表达式或逗号分隔的列表,后者产生一个元组)并将单个结果对象分配给每个目标列表,从左开始向右.
该部分还有一个相关警告适用于这种情况:
<块引用>警告:尽管赋值的定义意味着左侧和右侧之间的重叠是安全的"(例如 a, b = b, a
交换两个变量), 重叠内 分配给变量的集合是不安全的!例如,以下程序打印 [0, 2]
:
x = [0, 1]我 = 0i, x[i] = 1, 2打印 x
可以使用 bar.next_bar = bar = next_bar
并且这确实会产生最初想要的结果,但对任何人(包括一段时间后的原作者!)都感到遗憾,他们将不得不稍后阅读代码并为以下事实感到高兴,我相信 Tim 会使用这些词,如果他想到它们,
显式比可能令人困惑的极端情况要好.
I have some code like this:
def foo():
bar = initial_bar = Bar()
while True:
next_bar = Bar()
bar.next_bar = next_bar
bar = next_bar
return initial_bar
The intent being that a chain of Bar
s is formed which can be followed, linked-list style.
This was all very well; but through some misguided notion I wanted to cut it down by a line, compounding the assignments at the end of the loop into a single line.
def foo():
bar = initial_bar = Bar()
while True:
next_bar = Bar()
bar = bar.next_bar = next_bar
return initial_bar
Because bar = bar.next_bar = next_bar
will expand to bar.next_bar = next_bar
followed by effectively bar = bar.next_bar
. (Except that it doesn't.)
The problem is, this doesn't work; the "initial bar" returned does not have its next_bar
defined. I can easily enough work around it by going back to the more explicit two-line solution, but what's going on?
It's time to pull out dis
.
>>> import dis
>>> dis.dis(foo)
2 0 LOAD_GLOBAL 0 (Bar)
3 CALL_FUNCTION 0
6 DUP_TOP
7 STORE_FAST 0 (bar)
10 STORE_FAST 1 (initial_bar)
3 13 SETUP_LOOP 32 (to 48)
>> 16 LOAD_GLOBAL 1 (True)
19 POP_JUMP_IF_FALSE 47
4 22 LOAD_GLOBAL 0 (Bar)
25 CALL_FUNCTION 0
28 STORE_FAST 2 (next_bar)
5 31 LOAD_FAST 2 (next_bar)
34 DUP_TOP
35 STORE_FAST 0 (bar)
38 LOAD_FAST 0 (bar)
41 STORE_ATTR 2 (next_bar)
44 JUMP_ABSOLUTE 16
>> 47 POP_BLOCK
6 >> 48 LOAD_FAST 1 (initial_bar)
51 RETURN_VALUE
If you look closely at that, you'll see that in the critical line (line 5, see the numbers on the left, positions 31-47), it does this:
- Load
next_bar
(31) twice (34); - Write it (the first copy on the stack) to
bar
(35); - Write it (the second copy on the stack) to
bar.next_bar
(38, 41).
This is seen more obviously in a minimal test case.
>>> def a():
... b = c = d
...
>>> dis.dis(a)
2 0 LOAD_GLOBAL 0 (d)
3 DUP_TOP
4 STORE_FAST 0 (b)
7 STORE_FAST 1 (c)
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
See what it's doing. This means that b = c = d
is actually equivalent to b = d; c = d
. Normally this won't matter, but in the case mentioned originally, it does matter. It means that in the critical line,
bar = bar.next_bar = next_bar
is not equivalent to
bar.next_bar = next_bar
bar = next_bar
But rather to
bar = next_bar
bar.next_bar = next_bar
This is, in fact, documented, in section 6.2 of the Python documentation, Simple statements, Assignment statements:
An assignment statement evaluates the expression list (remember that this can be a single expression or a comma-separated list, the latter yielding a tuple) and assigns the single resulting object to each of the target lists, from left to right.
There is also a related warning in that section which applies to this case:
WARNING: Although the definition of assignment implies that overlaps between the left-hand side and the right-hand side are ‘safe’ (for example
a, b = b, a
swaps two variables), overlaps within the collection of assigned-to variables are not safe! For instance, the following program prints[0, 2]
:x = [0, 1] i = 0 i, x[i] = 1, 2 print x
It's possible to go for bar.next_bar = bar = next_bar
and that does produce the initially desired result, but have pity on anyone (including the original author some time later!) who will have to read the code later and rejoice in the fact that, in words that I'm sure Tim would have used had he thought of them,
Explicit is better than a potentially confusing corner-case.
这篇关于为什么分配给多个目标(标识符/属性)会产生奇怪的结果?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!