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
Trying to summarize and exemplify my comments here:
.append()
directlyThe 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]
.append()
directlyThe 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.
list
to allow the attribute to be callableWhich 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 :)