Swapping values in list with computed target indices

Question:

I have the following code for swapping 0 and 1 in the list:

b["".join(b).rfind("0")],b["".join(b).find("1")] = b["".join(b).find("1")],b["".join(b).rfind("0")]

If the value of b is ["1","0","1","1","1","1"] it becomes ["0","1","1","1","1","1"], swapping 0 and 1 as expected. However, if instead b starts as the previous result ["0","1","1","1","1","1"] the list remains unchanged after the assignment.
Why does this happen?

I want to understand internal mechanism of my code of why it is not working in opposite way. ie. the 0 is coming from RS to LS but not LS to RS.

Asked By: dragonfly

||

Answers:

It’s quite unclear what you’re trying to achieve. If you want to replace every 0 with 1 and vice versa, you can use list comprehension:

my_list = ['0', '1', '0', '1', '1', '0']
swapped_list = ['1' if x == '0' else '0' for x in my_list]
Answered By: Shai V.

The issue is the evaluation order in the assignment: The first part of the left-hand-side is evaluated and assigned to before the second part of the left-hand-side is evaluated at all. As a result, when the second assignment part runs the index search happens on the already partially modified b.

Python roughly evaluates the following steps for b = ["0","1","1","1","1","1"]:

  1. ... = b["".join(b).find("1")],b["".join(b).rfind("0")] => ... = "1", "0"
  2. b["".join(b).rfind("0")], ... = "1", ... => b[0], ... = "1", ...
    • => b = ["1","1","1","1","1","1"]
  3. ..., b["".join(b).find("1")] = ..., "0" => ..., b[0] = ..., "0"
    • => b = ["0","1","1","1","1","1"]

Notice how both 2. and 3. find the same index because 2. is fully evaluated before 3. already.
For the case of b = ["1","0","1","1","1","1"] the step 2. also runs first but does not change the position of the leading "1" and thus does not change the result of 3.

To reliably swap elements compute their indices beforehand:

>>> b = ["0","1","1","1","1","1"]
>>> # pre-compute indices before assignment
>>> idxs = "".join(b).rfind("0"), "".join(b).find("1")
>>> # use pre-computed indices in assignment
>>> b[idxs[0]], b[idxs[1]] = b[idxs[1]], b[idxs[0]]
>>> b
["1","0","1","1","1","1"]

This is a subtlety of assignment statements: Assignment is defined recursively on the target list, evaluating each assignment expression just as it is about to be assigned to.

Assignment of an object to a single target is recursively defined as follows.

  • [literal name assignment]
  • If the target is an attribute reference: The primary expression in the reference is evaluated. […]
  • If the target is a subscription: The primary expression in the reference is evaluated. […]
  • If the target is a slicing: The primary expression in the reference is evaluated. […]
Answered By: MisterMiyagi

This is because the assignment statement assigns the evaluated value on the right-hand side to the target list on the left hand side from left to right, with each target expression evaluated and assigned to before the next target expression gets evaluated and assigned to.

Excerpt from the documentation of assignment statement:

Although the definition of assignment implies that overlaps between
the left-hand side and the right-hand side are ‘simultaneous’ (for
example a, b = b, a swaps two variables), overlaps within the
collection of assigned-to variables occur left-to-right, sometimes
resulting in confusion. For instance, the following program prints [0,
2]:

x = [0, 1]
i = 0
i, x[i] = 1, 2         # i is updated, then x[i] is updated
print(x)

So when b is ['0', '1', '1', '1', '1', '1'], the right-hand side gets evaluated to 1, 0, then the first target expression on the left-hand side, b["".join(b).rfind("0")], gets evaluated to b[0], which gets assigned with 1, and then the next target expression, b["".join(b).rfind("1")], gets evaluated to b[0] because b[0] is already 1 at that point, and the entire statement effectively gets evaluated as:

b[0], b[0] = '1', '0'

resulting in no swap.

Answered By: blhsing
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.