Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python is adding values to multiple instances

Tags:

python

I have created a descriptor for lists.

After testing it seems that every time I append a value to a list of one instance, it is being added to another instance as well.
Even weirder, in the unittests it keeps appending to the list, and not resetting on every test.

My descriptor main class:

class Field(object):
    def __init__(self, type_, name, value=None, required=False):
        self.type = type_
        self.name = "_" + name
        self.required = required
        self._value = value

    def __get__(self, instance, owner):
        return getattr(instance, self.name, self.value)

    def __set__(self, instance, value):
        raise NotImplementedError

    def __delete__(self, instance):
        raise AttributeError("Can't delete attribute")

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        self._value = value if value else self.type()

Descriptor list class:

class ListField(Field):
    def __init__(self, name, value_type):
        super(ListField, self).__init__(list, name, value=[])
        self.value_type = value_type

    def __set__(self, instance, value):
        if not isinstance(value, list):
            raise TypeError("{} must be a list".format(self.name))
        setattr(instance, self.name, value)

    def __iter__(self):
        for item in self.value:
            yield item

    def __len__(self):
        return len(self.value)

    def __getitem__(self, item):
        return self.value[item]

    def append(self, value):
        if not isinstance(value, self.value_type):
            raise TypeError("Value is list {} must be of type {}".format(self.name, self.value_type))
        self.value.append(value)

Unittests:

# Class I created solely for testing purposes
class ListTestClass(object):
    l = ListField("l", int)


class TestListFieldClass(unittest.TestCase):
    def setUp(self):
        self.listobject = ListTestClass()

    def test_add(self):
        # The first number is added to the list
        self.listobject.l.append(2)

    def test_multiple_instances(self):
        # This test works just fine
        l1 = ListField("l1", int)
        l2 = ListField("l2", int)

        l1.append(1)
        l2.append(2)

        self.assertEqual(l1[0], 1)
        self.assertEqual(l2[0], 2)

    def test_add_multiple(self):
        # This test works just fine
        l1 = ListField("l1", int)
        l1.append(1)
        l1.append(2)

        self.assertEqual(l1[0], 1)
        self.assertEqual(l1[1], 2)

    def test_add_error(self):
        # This test works just fine
        with self.assertRaises(TypeError):
            l1 = ListField("l1", int)
            l1.append("1")

    def test_overwrite_list(self):
        # This test works just fine
        l1 = ListField("l1", int)
        l1 = []

        l1.append(1)

    def test_overwrite_error(self):
        # This test works just fine
        l1 = ListTestClass()
        l1.l.append(1)

        with self.assertRaises(TypeError):
            l1.l = "foo"

    def test_multiple_model_instances(self):
        # I create 2 more instances of ListTestClass
        l1 = ListTestClass()
        l2 = ListTestClass()

        l1.l.append(1)
        l2.l.append(2)

        self.assertEqual(l1.l[0], 1)
        self.assertEqual(l2.l[0], 2)

The last test fails

Failure
Traceback (most recent call last):
  File "/home/user/project/tests/test_fields.py", line 211, in test_multiple_model_instances
    self.assertEqual(l1.l[0], 1)
AssertionError: 2 != 1

When I look at the values for l1.1 and l2.l, they both have a list containing [2, 1, 2]

What am I missing here?

I looked to the memory addresses and it seems that the lists all point to the same object.

class ListFieldTest(object):
    lf1 = ListField("lf1", int)


class TestClass(object):
    def __init__(self):
        l1 = ListFieldTest()
        l2 = ListFieldTest()

        l1.lf1.append(1)
        l2.lf1.append(2)

        print(l1.lf1)
        print(l2.lf1)

        print(hex(id(l1)))
        print(hex(id(l2)))
        print(hex(id(l1.lf1)))
        print(hex(id(l2.lf1)))

This prints

[1, 2]
[1, 2]
0x7f987da018d0 --> Address for l1 
0x7f987da01910 --> Address for l2
0x7f987d9c4bd8 --> Address for l1.lf1
0x7f987d9c4bd8 --> Address for l2.lf1
like image 886
Johan Vergeer Avatar asked Dec 01 '25 07:12

Johan Vergeer


1 Answers

ListTestClass.l is a class attribute, so it is shared by all instances of the class. Instead, you should create an instance attribute, eg in the __init__ method:

class ListTestClass(object):
    def __init__(self):
        self.l = ListField("l", int)

Similar remarks apply to ListFieldTest. There may be other similar problems elsewhere in your code, I haven't examined it closely.

like image 142
PM 2Ring Avatar answered Dec 02 '25 21:12

PM 2Ring