0

I was just writing something a small code, something weird has been just happening.

class ndarray:
    counter = 1

    def __init__(self, array = list()):
        print(array)
        ndarray.counter += 1
        self.array = array
        print(ndarray.counter, 'ndarray.__init__', array, id(self))

    def __add__(self, nd):
        if len(self.array) != len(nd.array):
            raise ValueError('wrong lengths')
        
        new_nd = ndarray() # line P1
        print("ndarray.__add__", new_nd, id(new_nd))
        for i in range(len(self.array)):
            new_nd.array.append(self.array[i] + nd.array[i])

        return new_nd
    
    def __mul__(self, nd):
        if len(self.array) != len(nd.array):
            raise ValueError('wrong lengths')

        new_nd = ndarray() # line P2
        print("ndarray.__mul__", new_nd, id(new_nd))
        for i in range(len(self.array)):
            new_nd.array.append(self.array[i] * nd.array[i])

        return new_nd       

    def __repr__(self) -> str:
        return str(self.array)

I have this class definition that holds a list as instance attribute and I would like to do the following operation using operator overloads:

a = ndarray([1,2,3,4,5])
b = ndarray([5,6,7,8,9])
z1 = a * b
print("z1=", z1)

Everything works fine the code above. Please pay attention to the line P2, which creates an ndarray class object with an instance attribute self.array that holds an empty list [], as expected. However, when I try to run the following code, which does the second operator overloading operation after the multiplication:

a = ndarray([1,2,3,4,5])
b = ndarray([5,6,7,8,9])
c = ndarray([2,3,4,5,6])
z1 = a * b
print("z1=", z1)
z2 = z1 + c
print("z2=", z2)

I do not get what I expect. The problem is that whenever the second operator overloading is called, the new_nd = ndarray() call does not create an with empty list (although the default parameter is []) in self.array.

More specifically; when the z1 operation is done, the new_nd = ndarray() call creates a ndarray class object with an instance attribute self.array that holds an empty list []. However, in the second call of the operator overloading, in the z2operation, the new_nd = ndarray() call creates an instance attribute self.array that holds list of the previous result z1.

Interestingly, when I change the line P1 and line P2 as new_nd = ndarray([]), everything works as expected.

What is going on here?

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
ai-py
  • 177
  • 1
  • 7
  • I didn't understand yet what you expect to happen instead. – mkrieger1 May 15 '23 at 20:42
  • I would expect `new_nd = ndarray([])` and `new_nd = ndarray()` behave same as there is a default argument in the `__init__`. – ai-py May 15 '23 at 20:43
  • 1
    This is a common pitfall. The problem is that default arguments are evaluated *once*, when the function is first defined. Subsequent calls then use that same value over and over. When the default value is immutable this isn't a problem. When when it's mutable, like a list (normally written `[]` rather than `list()`), this generally causes unexpected behavior. The standard solution is to use a sentinel value for the default, e.g. `None`, then check for it in your method, using `[]` when seen. This will give a new list each time. – Tom Karzes May 15 '23 at 20:46
  • 2
    So replace `array = list()` in the parameter list with `array=None`, then in the method body add `if array is None: array = []`. That will eliminate the list sharing bug. – Tom Karzes May 15 '23 at 20:47
  • 1
    By the way, I know this is a duplicate question, but I don't have any references to previous ones. Maybe someone else can find one. – Tom Karzes May 15 '23 at 20:48
  • Thank you very much for the explanation @TomKarzes. I was not aware of this pitfall. I searched quite a lot but couldn't find it anywhere. – ai-py May 15 '23 at 20:49

0 Answers0