Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python 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 ? ) Thanks!

------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

like image 548
Jacqueline P. Avatar asked Feb 04 '19 01:02

Jacqueline P.


People also ask

What is @property in Python used for?

The @property is a built-in decorator for the property() function in Python. It is used to give "special" functionality to certain methods to make them act as getters, setters, or deleters when we define properties in a class.

What is property and setter in Python?

@property is used to get the value of a private attribute without using any getter methods. We have to put a line @property in front of the method where we return the private variable. To set the value of the private variable, we use @method_name. setter in front of the method. We have to use it as a setter.


1 Answers

Trying to summarize and exemplify my comments here. Hope this helps:

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 :)

like image 132
Samuel Dion-Girardeau Avatar answered Oct 26 '22 23:10

Samuel Dion-Girardeau