python-3.xoopproperties

Property setter on a python list


I am trying to leverage on Python @property to modify a class attribute , which is of type List. Most examples online assumes the attribute decorated by @property is a singular value, not a list that can be extended by setter.

To clarify question a bit : I don't just want to assign a value (a value of list of int) to property s, rather, I need to modify it (to append a new int to current list) .

My purpose is it is expected to have:

c = C()
c.s # [1,2,3] is the default value when instance c initiated.
c.s(5)

c.s # [1,2,3,5]

given implementation of C as below:

class C:
    def __init__(self):
        self._s = [1,2,3]
    @property
    def s(self):
        return self._s
    @s.setter
    def s(self, val):
        self._s.append(val)

Now if I do c.s(5), I will get

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-99-767d6971e8f3> in <module>()
----> 1 c.s(5)

TypeError: 'list' object is not callable

I have read the most relevant posts: Python property on a list and Python decorating property setter with list

but neither is appropriate to my case: __setitem__ can modify element of the list but i want to extend the list.

Use a global attribute is not acceptable for my current task.

In this regard, what is the best solution? (Or I should not expect @property on a mutable data structure from the beginning?)

------edited-----

@Samuel Dion-Girardeau suggested to

subclass list and define its call magic method

, but I am not sure I understand the solution. Should I do something like this :

class C(list):
     # somewhere when implementing class C
     def __call__(self):
         # even not sure what i need to do here


Solution

  • Trying to summarize and exemplify my comments here:

    Solution #1: Use a regular attribute, and .append() directly

    The purpose of a setter is not to extend the value of the attribute, it is to replace it. In this regard, a list isn't any more different than an int or a string. In your case, since the value is a mutable list, you can simply call the .append() method directly on it.

    class C():
        def __init__(self):
            self.s = [1, 2, 3]
    
    
    >>> c = C()
    >>> c.s
    [1, 2, 3]
    >>> c.s.append(1)
    >>> c.s
    [1, 2, 3, 1]
    >>> c.s = [0, 0]
    >>> c.s
    [0, 0]
    

    Solution #2: Use properties, and .append() directly

    The solution above works if there's nothing to check when getting/setting s. However if you need to use a property for some reason (e.g. you have some computation or checks to do, and want to prevent users from setting anything they want as s), you can do so with properties.

    In this instance, I'm preventing negative numbers in the list as an example of a validation.

    class C():
        def __init__(self):
            self._s = [1, 2, 3]
        @property
        def s(self):
            return self._s
        @s.setter
        def s(self, val):
            if any(x < 0 for x in val):
                raise ValueError('No negative numbers here!')
            self._s = val
    
    >>> c = C()
    >>> c.s
    [1, 2, 3]
    >>> c.s.append(1)
    >>> c.s
    [1, 2, 3, 1]
    >>> c.s = [0, 0]
    >>> c.s
    [0, 0]
    >>> c.s = [0, -1]
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 10, in s
    ValueError: No negative numbers here!
    

    Note where the error is thrown here: not if I call c.s(...), which your question assumed to be calling the setter, but rather when I assign to c.s, with c.s = ....

    Also note that the users of this class will modify _s indirectly, through the setter.

    Solution #3: Subclass list to allow the attribute to be callable

    Which I absolutely don't recommend at all because it breaks every expectation the users of this class would have, and I'm only providing it as a trivia, and because it allows the behaviour you asked for initially.

    class CallableList(list):
        # This is the method that actually gets called when you `c.s(...)`!
        def __call__(self, *args):
            self.append(*args)
    
    class C():
        def __init__(self):
            self._s = CallableList([1,2,3])
        @property
        def s(self):
            return self._s
        @s.setter
        def s(self, val):
            self._s = CallableList(val)
    
    >>> c = C()
    >>> c.s
    [1, 2, 3]
    >>> c.s(1)
    >>> c.s
    [1, 2, 3, 1]
    >>> c.s = [0, 0]
    >>> c.s
    [0, 0]
    >>> c.s(1337)
    >>> c.s
    [0, 0, 1337]
    

    Please don't do this, and if you do make sure it's not traceable back to me :)