2

Case A:

list1=[0, 1, 2, 3]

list2=list1

list1=list1+[4]

print(list1)

print(list2)

Output:

[0, 1, 2, 3, 4]

[0, 1, 2, 3]

(This non-mutating behavior also happens when concatenating a list of more than a single entry, and when 'multiplying' the list, e.g. list1=list1*2, in fact any type of "re-assignment" that performs an operation with an infix operator to the list and then assigns the result of that operation to the same list name using "=" )

In this case the original list object that list1 pointed to has not been altered in memory and list2 still points to it, another object has simply been created in memory for the result of the concatenation that list1 now points to (there are now two distinct, different list objects in memory)


Case B:

list1=[0, 1, 2, 3]

list2=list1

list1.append(4)

print(list1)

print(list2)

---

Output :

[0, 1, 2, 3, 4]

[0, 1, 2, 3, 4]

Case C:

list1=[0, 1, 2, 3]

list2=list1

list1[-1]="foo"

print(list1)

print(list2)

Outputs:

[0, 1, 2, 'foo']

[0, 1, 2, 'foo']

In case B and C the original list object that list1 points to has mutated, list2 still points to that same object, and as a result the value of list2 has changed. (there is still one single list object in memory and it has mutated).

This behavior seems inconsistent to me as a noob. Is there a good reason/utility for this?

EDIT :

I changed the list variables names from "list" and "list_copy" to "list1" and "list2" as this was clearly a very poor and confusing choice of names.

I chose Kabanus' answer as I liked how he pointed out that mutating operations are always(?) explicit in python.

In fact a short and simple answer to my question can be done summarizing Kabanus' answer into two of his statements :

-"In python mutating operations are explicit"

-"The addition[or multiplication] operator [performed on list objects] creates a new object, and doesn't change x[the list object] implicitly."

I could also add:

-"Every time you use square brackets to describe a list, that creates a new list object"

[this is from : http://www-inst.eecs.berkeley.edu/~selfpace/cs9honline/Q2/mutation.html , great explanations there on this topic]

Also I realized after Kabanus' answer how careful one must be tracking mutations in a program :

l=[1,2] 
z=l 
l+=[3]
z=z+[3] 

and

l=[1,2] 
z=l 
z=z+[3] 
l+=[3] 

Will yield completely different values for z. This must be a frequent source of errors isn't it?

I'm only in the beginning of my learning and haven't delved deeply into OOP concepts just yet, but I think I'm already starting to understand what the fuss around functional paradigm is about...

  • 1
    `list_copy=list` doesn't make a copy of the list, it just creates another variable name that refers to the original object. Also, you shouldn't use variable name that conflict with the names of built-ins, like `list`. – martineau Jul 19 '17 at 15:59
  • 1
    `list_copy` does *not* make a copy. You refer to the *same* list. – Willem Van Onsem Jul 19 '17 at 16:00
  • As others have pointed out, you are fundamentally confused about the nature of assignment. Please read [Facts and myths about Python names and values](https://nedbatchelder.com/text/names.html), that should clear things up. Assignment **never copies**. Second, by convention, augmented assignment operators work *in-place if possible*. Of course, if the data-structure is immutable, they don't. So, suppose you have a tuple, `t`, then `t += (1,2)` creates a new `tuple` object, unlike with `list`s, because `tuple`s are immutable. – juanpa.arrivillaga Jul 19 '17 at 16:06
  • @martineau and Willem I'm very well aware of the fact that this assignment only creates an alias of the same object and do not actually make a copy, you seem to be hung up on the fact that I used "list" and "list_copy" as variable names which I admit is confusing. Please assume that "list" is "x" and "list_copy" is "y", and you'll see my question makes more sense than you first assumed it did. So juanpa, I'm definitely not that "fundamentally confused" but your comment actually did answer my questions, so thank you very much! – David Lamorlette Jul 20 '17 at 15:11
  • @David: Sorry, you still seemed confused about several things. My main point was that you were not making a copy of the object named `list` (or `x`, or whatever) not what you named it. The second name you used, `list_copy` appeared to indicate you thought it was a copy of the first one—if not, why were you doing it? Lastly, although naming something `list` is definitely not a good idea, `list_copy` is fine because there's no built-in by that name (although the name itself was misleading, as I mentioned). – martineau Jul 20 '17 at 15:26
  • @martineau If you actually read the body of my question you will find that I wrote this for instance : "In case B and C the original list object that “list” points to has mutated, list_copy still points to that same object, and as a result the value of list_copy has changed. (there is still one single list object in memory and it has mutated)." Which definitely shows that I perfectly understand that "list" and "list_copy" point to the same list object in memory right after the list_copy=list assignment. You're still assuming things merely based on the variable names that I -poorly- chose. – David Lamorlette Jul 20 '17 at 15:59

2 Answers2

5
l += [4]

is equivalent to:

l.append(4)

and won't create a copy.

l = l + [4]

is an assignment to l, it first evaluates the right side of the assignment, then assigns the resulting object to name l. There is no way for this operation to be mutating l.

Update: I guess I haven't made myself clear enough. Of course operations on the RHS of the assignment may involve mutating the object that is the current value of LHS; but finally, the result of computing RHS is assigned to the LHS, thus overwriting any previous mutations. Example:

def increment_first(x):
    x[0] += 1
    return []

l = [ 1 ]
l = increment_first(l)

While the call to increment_first will increment l[0] as its side effect, the mutated list object will be lost anyway as soon as the value of RHS (in this case - an empty list) is assigned to l.

Błotosmętek
  • 12,717
  • 19
  • 29
  • 1
    There's no reason why, in principle, `alist + [4]` wouldn't mutate a list, it just happens not to. – juanpa.arrivillaga Jul 19 '17 at 16:03
  • 1
    Yes, the `__add__` method of `list` could easily do the operation in place and return `self`. It just doesn't. – kindall Jul 19 '17 at 16:04
  • 1
    While certainly possible, it would be strange for an infix operator to have side effect on the object, because what would the semantics of `[4] + alist` be? On an aside, I sometimes miss that python's standard library doesn't support a [`fluent`](https://en.wikipedia.org/wiki/Fluent_interface) style. I know it was a design decision to have side effect operations to return `None`, but `fluent` seems quite natural to me. – AChampion Jul 19 '17 at 18:42
2

This is by design. The point is python does not like non explicit side effects. Suppose this valid line in your file:

x=[1,2]
print(x+[3,4])

Note there is no assignment, but it's still a valid line. Do you expect x to have changed after that second line? For me it doesn't make sense.

That's what you're seeing - the addition operator creates a new object, and doesn't change x implicitly. If you feel it should, then what about:

[3,4]+x

Of course, addition does not change behavior in assignment, to avoid confusion. In python mutating operations are explicit:

x+=[3,4]

Or your example:

x[0]=1

Here you are explicitly asking to change a cell, i.e. explicit mutation. These things are consistent - an operation is always a mutation or it isn't, and it won't be both. Usually it makes sense as well, such as concatenating lists on the fly.

kabanus
  • 24,623
  • 6
  • 41
  • 74
  • "python does not like non explicit side effects" is actually one good, rememberable way to explain this behavior. And especially the fact that l+=[5] mutates the list while l=l+[5] will create another object in memory. – David Lamorlette Jul 20 '17 at 16:24