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.