Python sequentially unpacks tuple with assignment expression
The other day, I shadowed an interview with a data science candidate. The primary focus is obviously not on coding skills, but we do want to assess basic knowledge of the programming language of his choice. So, my colleague asked a very simple python question to warm him up. The question is: ‘how do you swap values of two variables wtihout using a temprary variable?’. To my surprise the candidate had no clue it is as simple as a, b = b, a
.
In most other languages this is not a valid statement. The reason it works in python is as follows:
- the expression on the right hand side gets evaluated. As a result, a tuple of two elements
(a, b)
is created. - then python unpack this tuple, assign the values to each variables on the left hand side sequentially in the left to right order.
So if a = 5; b = 3
. When python evaluates a, b = b, a
:
- It first create a tuple of
(3, 5)
- Then it assigns
3
toa
- Finally it assigns
5
tob
This is very handy and readable. However, users may think, as long as they aligned elements on the left with the corresponding elements on the right, the swap should always work. Indeed, if there is no error, code will be excuted, but not always as intened. The sequential order of unpacking should be considered when we write multiple assignments via tuple unpacking to avoid any ‘suprising’ behavior.
Let’s say that we want to do a simple linked list reversal. In other programming language, we will usually use a temprary variable to hold the next
node from curr
to make sure we can advance to it after we rewire curr.next
to prev
:
def reverse_linked_list(head):
prev, curr = None, head
while curr:
next_ = curr.next
curr.next = prev
prev = curr
curr = next_
return prev
With a quick drawing, you can picture how this rewiring works and verify it is correct.
With tuple unpacking, we can do the same thing in python like this:
while curr: curr.next, prev, curr = prev, curr, curr.next
This follows the assignments order from above with temprary variable swap pattern.
However this will also work.
while curr: prev, curr.next, curr = curr, prev, curr.next
At first read, one may feel the first two elements on both sides are out of order. But it does not matter, because the reference to the node objects are already stored in the tuple before the unpacking. However, this does not mean any order will work:
while curr: curr, prev, curr.next = curr.next, curr, prev
while curr: prev, curr, curr.next = curr, curr.next, prev
Both above will not work. Because the sequential unpacking. In the last two versions, after the first two unpacking, curr
will in both cases become referencing to the second node in the linked list. And the last unpacking assignment will wirte this node’s next
pointer to what prev
was at the begining of this unpacking happened, which is None
. As a result, the loop will throw error in the second iteration, as we will try to access .next
from None
.
If the above is hard to wrap your head around, the one-liner is roughly equivlent to:
while curr:
snapshot = (prev, curr, curr.next)
curr.next = snapshot[0]
prev = snapshot[1]
curr = snapshot[2]