-2

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?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
blueFast
  • 41,341
  • 63
  • 198
  • 344
  • 7
    Because you multiplied the nested list objects, which are mutable and are *updated in-place*. The second example multiplied integers, which are immutable, so can't be updated in-place and a new integer is returned. – Martijn Pieters Jul 05 '17 at 13:20
  • @MartijnPieters That they are mutable is clear, that they are updated in-place needs explanation – blueFast Jul 05 '17 at 13:22
  • @mpf82 not duplicate: I need to know what in-place updating means – blueFast Jul 05 '17 at 13:22
  • 2
    Test the following: `for i in a: print(hex(id(i)))`, notice the addresses. Now do this: `for i in a: i *= 2; print(hex(id(i)))`. Compare the addresses. You will realize that in the case where the elements are immutable (integers), the addresses change, whereas in the case where the elements are mutable (lists) the addresses are the same. That's why the elements are modified in one case but not the other: when mutable the same object is returned, when immutable, it's a copy in a different address, so modifying it will not modify the original. – dabadaba Jul 05 '17 at 13:28
  • Lists are mutable; ints are not. The `*=` operator on a list mutates the list. The `*=` operator on an int returns a new int (which is assigned the the variable on the left side of the operator). – khelwood Jul 05 '17 at 13:31
  • @khelwood I like that answer. It is not as formally correct as the one from Martijn, but it is more clear to me. – blueFast Jul 05 '17 at 13:33

3 Answers3

3

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.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
1

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.

Christian Dean
  • 22,138
  • 7
  • 54
  • 87
0

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.

magicleon94
  • 4,887
  • 2
  • 24
  • 53
  • I do not need to know what those operations do. I need to know why the original list is modified. – blueFast Jul 05 '17 at 13:24
  • @dangonfast Then why are you asking this question? Perhaps there's a larger overarching reason you're trying to do this that you need answered? – J0hn Jul 05 '17 at 13:25
  • @J0hn I never asked what the operations do, just why the list is modified. The operation is completely irrelevant to the question. – blueFast Jul 05 '17 at 13:29
  • I see your point now, and I think the answer you're looking for can be found in Martijn Pieters and dabadaba comments. – magicleon94 Jul 05 '17 at 13:30