Python mutation of lists in tuples

After learning about lists and box-and-pointer diagrams, I decided to create random stuff for myself and test out my knowledge. I am going to use the words shallow copy and suspected shallow copies as I'm not really sure whether they are correct by definition. My queries are in the reasons provide for the behaviour of such code, please tell me whether I'm thinking soundly.

Code A

from copy import *

x=[1,[2,[3,[4]]]] #normal copy/hardcopy
a=x
v=list(x) #suspected shallow copy
y=x.copy() #shallow copy
z=deepcopy(x) #theoretical deep copy
w=x[:] #suspected shallow copy

def test():
    print("Original:",x)
    print("hardcopy:",a)
    print("suspected shallow copy",v)
    print("shallow copy",y)
    print("deep copy:",z)
    print("suspected shallow copy",w)

x[1]=x[1]+[4]
test()

Output A:

Original: [1, [2, [3, [4]], 4]]
hardcopy: [1, [2, [3, [4]], 4]]
suspected shallow copy [1, [2, [3, [4]]]]
shallow copy [1, [2, [3, [4]]]]
deep copy: [1, [2, [3, [4]]]]
suspected shallow copy [1, [2, [3, [4]]]]

Code B

a=(1,2,[1,2,3])

def shallow_copy(x):
    tup=()
    for i in x:
        tup+=(i,)
    return tup


def hardcopy(x):
    return x

b=hardcopy(a)
c=shallow_copy(a)

a[2]+=[3] 

Output B: I see TypeError in IDLE here, but the mutation of the list element is still done, and across ALL a,b,c

Continuation from output B:

a[2][0]=a[2][0]+99
a,b,c

Output C:

((1, 2, [100, 2, 3, 3]), (1, 2, [100, 2, 3, 3]), (1, 2, [100, 2, 3, 3]))

Code D:

a=[1,2,(1,2,3)]

def shallow_copy(x):
    tup=[]
    for i in x:
        tup+=[i]
    return tup


def hardcopy(x):
    return x

b=hardcopy(a)
c=shallow_copy(a)
d=a.copy()
a[2]=a[2]+(4,)
a,b,c,d

Output D:

[1, 2, (1, 2, 3, 4)], [1, 2, (1, 2, 3, 4)], 
[1, 2, (1, 2, 3)], [1, 2, (1, 2, 3)]

From Output A, we observe the following: 1)For lists which have shallow copies, doing x[1]=x[1]+[4] does not affect the shallow copies. My reasons for the above could be

a) = followed by + does __add__ instead of __iadd__(which is +=), and doing __add__ should not modify the object, only changing the value for one pointer(x and its hardcopy in this case)

This is further supported in Output B but somehow contradicted in Output C, could be partly due to reason (b) below, but can't be too sure.

b) We executed this in the first layer(only 1 slice operator), maybe there's some kind of rule which prevents these elements from being modified.

This is supported by both Output B and Output C, though Output B might be argued to be in the first layer, think of it as increasing the elements in the 2nd layer, and it fits the above observation.

2)What is the reason why the TypeError appeared in Output B, but is still executed? I know that whether an Exception might be triggered is based on the final sequence you are actually changing(the list in this case), but why is there still TypeError: 'tuple' object does not support item assignment ?

I have presented my views for the above questions. I appreciate any thoughts(theoretical solutions preferably) on this question as I'm still relatively new to programming.

1 answer

  • answered 2018-04-14 14:30 progmatico

    To answer question 1, which looks complex but whose answer is probably quite simple:

    when you have a another name referencing the original object, you will see the changes in the original. Those changes will not reflect in other copies (being those either shallow or deep) if(!) you change the objects using the form x[1] = x[1] + [4]. This is because you are assigning a new object into x[1], instead of making an in-place change like in x[1].append(4).

    You can check that with the id() function.

    To answer your question 2, and adapted from the official docs:

    let's make

    a = (['hello'],)
    

    then

    a[0] += [' world']
    

    this is the same as

    a[0] = operator.iadd(a[0],[' world'])
    

    The iadd changes the list in place, but then the assignment fails because you can't assign to a tuple (immutable type) index.

    If you make

    a[0] = a[0] + [' world']
    

    the concatenation goes into a new list object, then the assignment to the tuple index fails too. But the new object gets lost. a[0] wasn't changed in place.

    To clarify OP's comment, directly from the docs in here it says that

    Many operations have an “in-place” version. Listed below are functions providing a more primitive access to in-place operators than the usual syntax does; for example, the statement x += y is equivalent to x = operator.iadd(x, y). Another way to put it is to say that z = operator.iadd(x, y) is equivalent to the compound statement z = x; z += y.

    In those examples, note that when an in-place method is called, the computation and assignment are performed in two separate steps. The in-place functions listed below only do the first step, calling the in-place method. The second step, assignment, is not handled.

    As for your Output D: Writing

    b = hardcopy(a)
    

    does nothing more than writing

    b = a
    

    really, b is a new name referencing the same object that a references. This is because a is mutable and so a reference pointing to the original object is passed into local function name x. Returning x just returns the same reference into b.

    That's why you see further changes in a reflected in b. Again you make a[2] a new different object tuple by assignment, so now a[2] and b[2] reference a new tuple (1,2,3,4), while c and d still reference the old tuple object. And now because they are tuples you can't change them in place, like lists.