Why are several goals assigned (identifier / attribute) leading to strange results?

I have a 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 

It is assumed that a chain of Bar , which may be followed by a linked list style.

It was very good; but through some kind of erroneous concept, I would like to reduce it to a line, exacerbating the assignments at the end of the cycle in one 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 and then effectively bar = bar.next_bar . (Except that it is not.)

The problem is that this does not work; the returned "start line" does not have its next_bar . I can easily get around this by returning to a more explicit two-line solution, but what happens?

+4
source share
1 answer

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 this, you will see that in the critical line (line 5 see numbers on the left, positions 31-47), it does this:

  • Download next_bar (31) twice (34);
  • Write it (the first instance on the stack) to bar (35);
  • Write it (second copy bar.next_bar stack) in bar.next_bar (38, 41).

This is more obvious 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 he does. This means that b = c = d actually equivalent to b = d; c = d b = d; c = d . Usually this does not matter, but in the case mentioned originally, it matters. This means that in the critical line

 bar = bar.next_bar = next_bar 

not equivalent

 bar.next_bar = next_bar bar = next_bar 

Instead

 bar = next_bar bar.next_bar = next_bar 

This is actually documented in section 6.2 of the Python documentation, Simple Instructions, Assignment statements :

The assignment operator evaluates the list of expressions (remember that this can be a single expression or a list separated by commas, the last of which has a tuple) and assigns a single result object to each of the target lists, from left to right .

There is also a warning in this section that applies to this case:

A WARNING. Although the definition of assignment means that overlaps between the left and right sides are “safe (for example, a, b = b, a sweeps two variables), overlaps in the collection of assigned variables are not safe! For example, the following program prints [0, 2] :

 x = [0, 1] i = 0 i, x[i] = 1, 2 print x 

You can go to bar.next_bar = bar = next_bar , and it really will bring the desired result initially, but pity someone (including the author of the original after a while!) Who will have to read the code later and enjoy that fact, in words that, I I'm sure Tim would use it if he thought about them,

Explicit is better than a potentially tangled corner case.

+5
source

All Articles