Simultaneous assignment indexing different list elements in Python [duplicate]
Asked Answered
M

3

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"

Mightily answered 4/8, 2021 at 1:56 Comment(0)
S
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
Stoss answered 4/8, 2021 at 2:18 Comment(3)
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)?Mightily
@SopeadeLanlehin the right hand side is a tuple literal, so it gets evaluated from left to right.Stoss
but as you explained the answer should be as expected before [3, 2, 1, 4], but the real behavior is [3, 2, 4, 3] also your last test shows a different result to the previous explanationSmart
O
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!

Osyth answered 4/8, 2021 at 2:34 Comment(3)
Still thinking over your comment @Tom. Theres a lot in hereMightily
@SopeadeLanlehin yes, maybe a few too many threads to pull in here :P but I can try to explain more if you have questions!Osyth
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!Mightily
V
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.
Vale answered 4/8, 2021 at 2:19 Comment(5)
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 !!Mightily
@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)Stoss
@Stoss hmmm. Got it. Got it (at least i think so). So it technically closer to eval(d)--> eval(c)--->assgn(a)--->assgn(b)Mightily
@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)Stoss
Ah. Okay, it creates the tuple 1st b4 doing any Assignment. I got it now! Thanks so much @StossMightily

© 2022 - 2024 — McMap. All rights reserved.