Why is assigning to multiple targets (identifier/a

2019-06-25 11:39发布

问题:

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 Bars 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?

回答1:

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.