14
>>arr = [4, 2, 1, 3]
>>arr[0], arr[arr[0]-1] = arr[arr[0]-1], arr[0]
>>arr

Result I expect >>[3, 2, 1, 4]

Result I get >>[3, 2, 4, 3]

Basically I'm trying to swap the #4 and #3 (In my actual problem, the index wont be 0, but rather an iterator "i" . So I cant just do arr[0], arr[3] = arr[3], arr[0]) I thought I understood simultaneous assignment fairly well. Apparently I was mistaken. I don't understand why arr[arr[0]-1] on the left side of the assignment is evaluating to arr[2] instead of arr[3]. If the assignments happen simultaneously (evaluated from the right),

arr[0] (within the index of the 2nd element on the left)should still be "4"

arr[0] -1 (the index of the 2nd element on the left) should thus be "3"

John Kugelman
  • 349,597
  • 67
  • 533
  • 578

3 Answers3

11

Because the target list does not get evaluated simultaneously. Here is the relevant section of the docs:

The object must be an iterable with the same number of items as there are targets in the target list, and the items are assigned, from left to right, to the corresponding targets.

Two things to keep in mind, the right hand side evaluates the expression first. So on the RHS, we first create the tuple :

 (3, 4)

Note, that is done left to right. Now, the assignment to each target in the target list on the left is done in order:

arr[0] = 3

Then the next target, arr[0] is 3, and 3-1 is 2

arr[2] = 4

So a simple solution is to just to compute the indices first before the swap:

>>> arr = [4, 2, 1, 3]
>>> i, j = arr[0] - 1, 0
>>> arr[j], arr[i] = arr[i], arr[j]
>>> arr
[3, 2, 1, 4]

Here is a demonstration using a verbose list that we can define easily:

>>> class NoisyList(list):
...     def __getitem__(self, item):
...         value = super().__getitem__(item)
...         print("GETTING", item, "value of", value)
...         return value
...     def __setitem__(self, item, value):
...         print("SETTING", item, 'with', value)
...         super().__setitem__(item, value)
...
>>> arr = NoisyList([4, 2, 1, 3])
>>> arr[0], arr[arr[0]-1] = arr[arr[0]-1], arr[0]
GETTING 0 value of 4
GETTING 3 value of 3
GETTING 0 value of 4
SETTING 0 with 3
GETTING 0 value of 3
SETTING 2 with 4
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • Thank you very much! So my understanding of simultaneous assignment was incomplete afterall. So the right side of assignment operator '='is evaluated 1st(from rightmost to leftmost? or does it not matter). Then left side of the assignment operator '=' is evaluated next(from leftmost to rightmost)? – Sopeade Lanlehin Aug 04 '21 at 02:23
  • @SopeadeLanlehin the right hand side is a *tuple literal*, so it gets evaluated from *left to right*. – juanpa.arrivillaga Aug 04 '21 at 02:41
3

The replacement of the two values isn't truly simultaneous; they are handled in order from left to right. So altering arr during that process is leading to this behavior.

Consider this alternative example:

>>> arr = [1, 2, 3]
>>> arr[0], arr[arr[0]] = 10, 5
...

With a hypothetical simultaneous reassignment, we try to replace the first value of arr with 10, and then the arr[0]th (aka 1st) element with 5. So hopefully, we get [10, 5, 3]. But this fails with IndexError: list assignment index out of range. If you then inspect arr after this error:

>>> arr
[10, 2, 3]

The first assignment was completed, but the second failed. When it came to the second assignment (after the comma), the actual arr[0]th (aka 10th) value cannot be found (b/c the list isn't that long).

This behavior can also be seen by clearly specifying the second assignment to fail (still by specifying an index out of range):

>>> arr = [1, 2, 3]
>>> arr[0], arr[99] = 5, 6
# Same index error, but arr becomes [5, 2, 3]

This feels reminiscent of modifying a list you are iterating over, which is sometimes doable but often discouraged because it leads to issues like what you are seeing.

One alternative is to create a copy (this is sometimes a solution for modifying the list you are iterating over), and use that for referencing values in arr:

>>> arr = [4, 2, 1, 3]
>>> copy = arr.copy()
>>> arr[0], arr[copy[0]-1] = copy[copy[0]-1], copy[0]
>>> arr
[3, 2, 1, 4]

Though it is pretty ugly here. The alternative in the accepted answer is much nicer, or this idiomatic approach should probably work as well!

Tom
  • 8,310
  • 2
  • 16
  • 36
  • Still thinking over your comment @Tom. Theres a lot in here – Sopeade Lanlehin Aug 04 '21 at 02:44
  • @SopeadeLanlehin yes, maybe a few too many threads to pull in here :P but I can try to explain more if you have questions! – Tom Aug 04 '21 at 02:49
  • 1
    I've got it! Very well explained, and great reference to the "modifying list you are iterating over" similarities. For now (and in the future) I just might stick to the "less pythonic" approach of creating temp variable if I know i'm modifying the variables being assigned. Copy is not bad either, but i find temp easier to read. Thanks sooo much Sir! – Sopeade Lanlehin Aug 04 '21 at 03:01
2

Ok, this happens because the arr[0] is changed to 3 when you assign arr[arr[0]-1] to it. And after that, when python takes a look at arr[arr[0]-1] and tries to assign it the value of arr[0] if finds arr[0] to be 3 because in the previous assignment you have changed it. A small demonstration:

arr = [4, 2, 1, 3]

arr[0], arr[arr[0]-1] = arr[arr[0]-1], arr[0]
 │                           │ 
 └──────────┬────────────────┘
            │
            │
      these are done first 
      and the list becomes:
      [3, 2, 1, 3]

Next when the python takes a look at
                     these two, it:
                         │
            ┌────────────┴───────────────┐
            │                            │ 
arr[0], arr[arr[0]-1] = arr[arr[0]-1], arr[0]

it finds the `arr[0]` to be `3` so, it assigns `3` to the `3rd` 
element because `arr[arr[0]-1]` is 3.
user3840170
  • 26,597
  • 4
  • 30
  • 62
Parvat . R
  • 751
  • 4
  • 21
  • Thank you sooo much @Parvat! Would accept this excellent answer, but already accepted a comparable answer above. This definitely helped clarify the sequence of actions. (Tried using the dis.dis module to troubleshoot, but couldn't fully understand the sequence that way). So if each element is a, b = c, d the order is basically, (eval)c--> (assgn)a-->(eval)d--->(ass)b !! – Sopeade Lanlehin Aug 04 '21 at 02:31
  • @SopeadeLanlehin technically, the right hand side is evaluated first, `c, d` is *merely a tuple* (although CPython optimized I think the simplest case of a pair to just use the call stack, but that is an implementation detail) – juanpa.arrivillaga Aug 04 '21 at 02:33
  • @juanpa.arrivillaga hmmm. Got it. Got it (at least i think so). So it technically closer to eval(d)--> eval(c)--->assgn(a)--->assgn(b) – Sopeade Lanlehin Aug 04 '21 at 02:35
  • @SopeadeLanlehin Nope, the right hand side is a *tuple*. It goes something like, "construct tuple, first item is eval(c), second item is eval(d)" --> assgn(a) --> assgn(b) – juanpa.arrivillaga Aug 04 '21 at 02:42
  • Ah. Okay, it creates the tuple 1st b4 doing any Assignment. I got it now! Thanks so much @juanpa.arrivillaga – Sopeade Lanlehin Aug 04 '21 at 02:46