Search code examples
pythonpropertiespython-descriptors

Reflect Python list change in string attribute


I'm searching an elegant (an efficient) way to implement the following:

I have a class storing a list of values as a string (with a separator, eg.: " - "). I use a property (getter and setter) to convert this string into a Python list.

class C(object):
    def __init__(self, values):
        """
        Construct C with a string representing a list of values.

        :type values: str
        :param values: List of values
        """
        self.values = values

    @property
    def value_list(self):
        """ Values as list """
        return self.values.split(" - ")

    @value_list.setter
    def value_list(self, seq):
        self.values = " - ".join(seq)

Getting / Setting the property is OK:

c = C("one - two")
assert c.value_list == ["one", "two"]

c.value_list = ["one", "two", "three"]
assert c.values == "one - two - three"

But I'm looking for something (may be another kind of list) to automatically reflect the changes in the list.

c.value_list.append("four")
assert c.values == "one - two - three - four"

Traceback (most recent call last):
...
AssertionError

Currently, I implement my own list class inheriting collections.MutableSequence with a callback system. Is there a better way to do that?

EDIT: my current solution

I use a list with a "on_change" handler, like this:

class MyList(collections.MutableSequence):
    """ A ``list`` class with a "on_change" handler. """

    def __init__(self, *args):
        self._list = list(*args)

    def on_change(self, seq):
        pass

    def __getitem__(self, index):
        return self._list.__getitem__(index)

    def __len__(self):
        return self._list.__len__()

    def __setitem__(self, index, value):
        self._list.__setitem__(index, value)
        self.on_change(self._list)

    def __delitem__(self, index):
        self._list.__delitem__(index)
        self.on_change(self._list)

    def insert(self, index, value):
        self._list.insert(index, value)
        self.on_change(self._list)

Then I need to modify my C class to implement this handler to reflect the changes.

The new version of the class is:

class C(object):
    def __init__(self, values):
        """
        Construct C with a string representing a list of values.

        :type values: str
        :param values: List of values
        """
        self.values = values

    def _reflect_changes(self, seq):
        self.values = " - ".join(seq)

    @property
    def value_list(self):
        """ Values as list """
        my_list = MyList(self.values.split(" - "))
        my_list.on_change = self._reflect_changes
        return my_list

    @value_list.setter
    def value_list(self, seq):
        self.values = " - ".join(seq)

That way, any change in the list in reflected in the values attribute:

c = C("one - two")

c.value_list.append("three")
assert c.values == "one - two - three"

c.value_list += ["four"]
assert c.values == "one - two - three - four"

Solution

  • Maybe I'm oversimplifying your problem because you've oversimplified your first example, but I'm going to agree with some of the commenters and propose that you change around the way you store this. Store the list, produce the string on demand.

    In [1]: class C(object):
       ...:     def __init__(self, values):
       ...:         self.values = values.split(' - ')
       ...:     @property
       ...:     def value_str(self):
       ...:         return ' - '.join(self.values)
       ...:
    
    In [2]: c = C('one - two - three')
    
    In [3]: c.values
    Out[3]: ['one', 'two', 'three']
    
    In [4]: c.values.append('four')
    
    In [5]: c.value_str
    Out[5]: 'one - two - three - four'