Search code examples
pythonlistaugmented-assignment

Why does the original list change?


Running this:

a = [[1], [2]]
for i in a:
    i *= 2
print(a)

Gives

[[1, 1], [2, 2]]

I would expect to get the original list, as happens here:

a = [1, 2]
for i in a:
    i *= 2
print(a)

Which gives:

[1, 2]

Why is the list in the first example being modified?


Solution

  • You are using augmented assignment statements. These operate on the object named on the left-hand side, giving that object the opportunity to update in-place:

    An augmented assignment expression like x += 1 can be rewritten as x = x + 1 to achieve a similar, but not exactly equal effect. In the augmented version, x is only evaluated once. Also, when possible, the actual operation is performed in-place, meaning that rather than creating a new object and assigning that to the target, the old object is modified instead.

    (bold emphasis mine).

    This is achieved by letting objects implement __i[op]__ methods, for =* that's the __imul__ hook:

    These methods are called to implement the augmented arithmetic assignments (+=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=). These methods should attempt to do the operation in-place (modifying self) and return the result (which could be, but does not have to be, self).

    Using *= on a list multiplies that list object and returns the same list object (self) to be 'assigned' back to the same name.

    Integers on the other hand are immutable objects. Arithmetic operations on integers return new integer objects, so int objects do not even implement the __imul__ hook; Python has to fall back to executing i = i * 3 in that case.

    So for the first example, the code:

    a = [[1], [2]]
    for i in a:
        i *= 2
    

    really does this (with the loop unrolled for illustration purposes):

    a = [[1], [2]]
    i = a[0].__imul__(2)  # a[0] is altered in-place
    i = a[1].__imul__(2)  # a[1] is altered in-place
    

    where the list.__imul__ method applies the change to the list object itself, and returns the reference to the list object.

    For integers, this is executed instead:

    a = [1, 2]
    i = a[0] * 2  # a[0] is not affected
    i = a[1] * 2  # a[1] is not affected
    

    So now the new integer objects are assigned to i, which is independent from a.