Why does the original list change?

353 Views Asked by At

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?

3

There are 3 best solutions below

0
On BEST ANSWER

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.

4
On

In the first case your i in the for loop is a list. So you're telling python hey, take the ith list and repeat it twice. You're basically repeating the list 2 times, this is what the * operator does to a list.
In the second case your i is a value, so you're applying * to a value, not a list.

0
On

The reason your results are different for each example is because list are mutable, but integers are not.

That means when you you modify an integer object in place, the operator must return a new integer object. However, since list are mutable, the changes are simply added to already existing list object.

When you used *= in the for-loop in the first example, Python modify the already existing list. But when you used *= with the integers, a new integer object had to be returned.

This can also be observed with a simple example:

>>> a = 1
>>> b = [1]
>>> 
>>> id(a)
1505450256
>>> id(b)
52238656
>>> 
>>> a *= 1
>>> b *= 1
>>> 
>>> id(a)
1505450256
>>> id(b)
52238656
>>>

As you can see above, the memory address for a changed when we multiplied it in-place. So *= returned a new object. But the memory for the list did not change. That means *= modified the list object in-place.