Search code examples
pythonoopinheritancedeep-copy

Shallow copy instead of new object in Python


I'm trying to create a new object of a class inside this same class, however instead of creating a whole new object it simply creates a new reference to the same object I'm currently working in.

So if I change the value of one object, it changes the value in the other as well - even though I probably should have 2 completely different objects.

Using the copy.deepcopy() method fixes the issue with the reference, but I guess that it should work differently as well.

How does my code behave in this specific implementation? Is there a reason for it to create a shallow copy of the same object even though the code probably should create a new instance of it?

This is a slightly reduced code snippet:

class Vec4():
    def __init__(self, x = 0, y = 0, z = 0, w = 0):
        self.values = [x,y,z,w]

    def __str__(self):
        return str(self.values[0]) + ' ' + str(self.values[1]) + ' ' + str(self.values[2]) + ' ' + str(self.values[3])

    def setValue(self, index, value):
        self.values[index] = value


    def scalar(self, vector):
        """returns the result of the scalar multiplication"""
        result = 0
        for u in range(4):
            result += self.values[u] * vector.values[u]
        return result




class Matrix4():
    def __init__(self, row1 = Vec4(), row2 = Vec4(), row3 = Vec4(), row4 = Vec4()):
        self.m_values = [row1,row2,row3,row4]
        self.trans_values = [Vec4(),Vec4(),Vec4(),Vec4()]
        self.set_transp_matrix()

    def __str__(self):
        return self.m_values[0].__str__() + '\n' + self.m_values[1].__str__() + '\n' + self.m_values[2].__str__() + '\n' + self.m_values[3].__str__()

    def setIdentity(self):

        identity = Matrix4(Vec4(1,0,0,0),
                       Vec4(0,1,0,0),
                       Vec4(0,0,1,0),
                       Vec4(0,0,0,1))
        for i in range(4):
            for j in range(4):
                self.m_values[i].values[j] = identity.m_values[i].values[j]

    def set_transp_matrix(self):
         for t in range(4):
            for s in range(4):
                self.trans_values[t].values[s] = self.m_values[s].values[t]

    def get_trans_matrix(self):
        return self.trans_values[0].__str__() + '\n' + self.trans_values[1].__str__() + '\n' + self.trans_values[2].__str__() + '\n' + self.trans_values[3].__str__()

    def mulM(self, m):

        print(self, "\n")
        matrixResult = Matrix4()
        print(matrixResult, "\n")
        for row in range(4):  # rows of self
            for element in range(4):
                value = self.m_values[row].scalar(m.trans_values[element])
                matrixResult.m_values[row].setValue(element, value)
        return matrixResult


class ScaleMatrix(Matrix4):

    def __init__(self, m_scale = Vec4(1,1,1), *args, **kwargs):
        super(ScaleMatrix, self).__init__(*args, **kwargs)
        self.m_scale = m_scale
        self.update()

    def getScale(self):
        """Returns the scale vector, only x, y and z are relevant"""
        return self.m_scale

    def setScale(self, v):
        """Sets the scale vector, only x, y and z are relevant"""
        self.m_scale = v
        self.update()

    def update(self):
        """Calculates the scale matrix"""

        self.setIdentity()

        for i in range(3):
            self.m_values[i].values[i] = self.getScale().values[i]

        return self


if __name__ == "__main__":
    #Simple Constructor and Print
    a = Vec4(1,2,3,4)
    b = Vec4(5,6,7,8)
    c = Vec4(9,10,11,12)
    d = Vec4(13,14,15,16)


    A = Matrix4(a, b, c, d)
    D = ScaleMatrix()
    D.setScale(Vec4(3, 4, 5, 1))

    print(D.mulM(A))

The issue is in the class Matrix4, method mulM() where matrixResult = Matrix4() should create a whole new instance of Matrix4() (where all values of Vec4() should be 0) instead of simply copying the self object. The output of print shows the following:

3 0 0 0
0 4 0 0
0 0 5 0
0 0 0 1 

3 0 0 0
0 4 0 0
0 0 5 0
0 0 0 1 

3 6 51 672
20 64 508 6688
45 140 1170 15340
13 40 334 4396

So the second matrix should not be equal to the first one. However, the issue does not occur if I create a normal Matrix4() object instead of the ScaleMatrix() object which extends Matrix4() in the very end of the code snippet above.

Python v.3.6.4


Solution

  • Python evaluates the default parameters when a function is defined. When you define:

    class Matrix4():
        def __init__(self, row1 = Vec4() ...
    

    you create a Vec4 instance, and this instance will now be used as default value each time this __init__ method gets called.

    The first time you create a Matrix4 instance, __init__ will execute, and this instance gets refered to by the name row1

    Then you have:

        self.m_values = [row1,row2,row3,row4]
    

    So this instance is now refered to by self.m_values[0].

    Later, in ScaleMatrix.update, you update this Vec4 instance:

    for i in range(3):
                self.m_values[i].values[i] = self.getScale().values[i]
    

    The next time you will call Matrix4.__init__ without parameters, the default value will be used, and this is the Vec4 that you just updated.

    You have a similar behaviour when you use an empty list as default parameter, see “Least Astonishment” and the Mutable Default Argument .

    The usual way to avoid this problem is to avoid using mutable objects as default parameters. You could do:

    class Matrix4():
        def __init__(self, row1 = None, row2 = None, row3 = None, row4 = None):
            if row1 is None:
                row1 = Vec4()
            if row2 is None:
                row2 = Vec4()
            if row3 is None:
                row3 = Vec4()
            if row4 is None:
                row4 = Vec4()
            self.m_values = [row1,row2,row3,row4]
            self.trans_values = [Vec4(),Vec4(),Vec4(),Vec4()]
            self.set_transp_matrix()
    

    which gives as output:

    3 0 0 0
    0 4 0 0
    0 0 5 0
    0 0 0 1 
    
    0 0 0 0
    0 0 0 0
    0 0 0 0
    0 0 0 0 
    
    3 6 9 12
    20 24 28 32
    45 50 55 60
    13 14 15 16