bgneal@4: A C & Python chained assignment gotcha bgneal@4: ###################################### bgneal@4: bgneal@4: :date: 2012-12-27 14:45 bgneal@4: :tags: Python, C++ bgneal@4: :slug: a-c-python-chained-assignment-gotcha bgneal@4: :author: Brian Neal bgneal@4: bgneal@4: Late last night I had a marathon debugging session where I discovered I had bgneal@4: been burned by not fully understanding chaining assignment statements in bgneal@4: Python. I was porting some C code to Python that had some chained assignment bgneal@4: expressions. C and C++ programmers are well used to this idiom which has the bgneal@4: following meaning: bgneal@4: bgneal@4: .. sourcecode:: c bgneal@4: bgneal@4: a = b = c = d = e; // C/C++ code bgneal@4: bgneal@4: // The above is equivalent to this: bgneal@4: bgneal@4: a = (b = (c = (d = e))); bgneal@4: bgneal@4: This is because in C, assignments are actually expressions that return a value, bgneal@4: and they are right-associative. bgneal@4: bgneal@4: I knew that Python supported this syntax, and I had a vague memory that it was bgneal@4: not the same semantically as C, but I was in a hurry. After playing a bit in the bgneal@4: shell I convinced myself this chained assignment was doing what I wanted. My bgneal@4: Python port kept this syntax and I drove on. A huge mistake! bgneal@4: bgneal@4: Hours later, of course, I found out the hard way the two are not exactly bgneal@4: equivalent. For one thing, in Python, assignment is a statement, not an bgneal@4: expression. There is no 'return value' from an assignment. The Python syntax bgneal@4: does allow chaining for convenience, but the meaning is subtly different. bgneal@4: bgneal@4: .. sourcecode:: python bgneal@4: bgneal@4: a = b = c = d = e # Python code bgneal@4: bgneal@4: # The above is equivalent to these lines of code: bgneal@4: a = e bgneal@4: b = e bgneal@4: c = e bgneal@4: d = e bgneal@4: bgneal@4: Now usually, I suspect, you can mix the C/C++ meaning with Python and not get bgneal@4: tripped up. But I was porting some tricky red-black tree code, and it made bgneal@4: a huge difference. Here is the C code first, and then the Python. bgneal@4: bgneal@4: .. sourcecode:: c bgneal@4: bgneal@4: p = p->link[last] = tree_rotate(q, dir); bgneal@4: bgneal@4: // The above is equivalent to: bgneal@4: bgneal@4: p = (p->link[last] = tree_rotate(q, dir)); bgneal@4: bgneal@4: bgneal@4: The straight (and incorrect) Python port of this code: bgneal@4: bgneal@4: .. sourcecode:: python bgneal@4: bgneal@4: p = p.link[last] = tree_rotate(q, d) bgneal@4: bgneal@4: # The above code is equivalent to this: bgneal@4: temp = tree_rotate(q, d) bgneal@4: p = temp # Oops bgneal@4: p.link[last] = temp # Oops bgneal@4: bgneal@4: Do you see the problem? It is glaringly obvious to me now. The C and Python bgneal@4: versions are not equivalent because the Python version is executing the code in bgneal@4: a different order. The flaw comes about because ``p`` is used multiple times in bgneal@4: the chained assignment and is now susceptible to an out-of-order problem. bgneal@4: bgneal@4: In the C version, the tree node pointed at by ``p`` has one of its child links bgneal@4: changed first, then ``p`` is advanced to the value of the new child. In the bgneal@4: Python version, the tree node referenced by the name ``p`` is changed first, bgneal@4: and then its child link is altered! This introduced a very subtle bug that cost bgneal@4: me a few hours of bleary-eyed debugging. bgneal@4: bgneal@4: Watch out for this when you are porting C to Python or vice versa. I already bgneal@4: avoid this syntax in both languages in my own code, but I do admit it is nice bgneal@4: for conciseness and let it slip in occasionally.