Dataclasses and property decorator
It sure does work:
from dataclasses import dataclass@dataclassclass Test: _name: str="schbell" @property def name(self) -> str: return self._name @name.setter def name(self, v: str) -> None: self._name = vt = Test()print(t.name) # schbellt.name = "flirp"print(t.name) # flirpprint(t) # Test(_name='flirp')
In fact, why should it not? In the end, what you get is just a good old class, derived from type:
print(type(t)) # <class '__main__.Test'>print(type(Test)) # <class 'type'>
Maybe that's why properties are nowhere mentioned specifically. However, the PEP-557's Abstract mentions the general usability of well-known Python class features:
Because Data Classes use normal class definition syntax, you are free to use inheritance, metaclasses, docstrings, user-defined methods, class factories, and other Python class features.
TWO VERSIONS THAT SUPPORT DEFAULT VALUES
Most published approaches don't provide a readable way to set a default value for the property, which is quite an important part of dataclass. Here are two possible ways to do that.
The first way is based on the approach referenced by @JorenV. It defines the default value in _name = field()
and utilises the observation that if no initial value is specified, then the setter is passed the property object itself:
from dataclasses import dataclass, field@dataclassclass Test: name: str _name: str = field(init=False, repr=False, default='baz') @property def name(self) -> str: return self._name @name.setter def name(self, value: str) -> None: if type(value) is property: # initial value not specified, use default value = Test._name self._name = valuedef main(): obj = Test(name='foo') print(obj) # displays: Test(name='foo') obj = Test() obj.name = 'bar' print(obj) # displays: Test(name='bar') obj = Test() print(obj) # displays: Test(name='baz')if __name__ == '__main__': main()
The second way is based on the same approach as @Conchylicultor: bypassing the dataclass machinery by overwriting the field outside the class definition.
Personally I think this way is cleaner and more readable than the first because it follows the normal dataclass idiom to define the default value and requires no 'magic' in the setter.
Even so I'd prefer everything to be self-contained... perhaps some clever person can find a way to incorporate the field update in dataclass.__post_init__()
or similar?
from dataclasses import dataclass@dataclassclass Test: name: str = 'foo' @property def _name(self): return self._my_str_rev[::-1] @_name.setter def _name(self, value): self._my_str_rev = value[::-1]# --- has to be called at module level ---Test.name = Test._namedef main(): obj = Test() print(obj) # displays: Test(name='foo') obj = Test() obj.name = 'baz' print(obj) # displays: Test(name='baz') obj = Test(name='bar') print(obj) # displays: Test(name='bar')if __name__ == '__main__': main()
An @property
is typically used to store a seemingly public argument (e.g. name
) into a private attribute (e.g. _name
) through getters and setters, while dataclasses generate the __init__()
method for you. The problem is that this generated __init__()
method should interface through the public argument name
, while internally setting the private attribute _name
.This is not done automatically by dataclasses.
In order to have the same interface (through name
) for setting values and creation of the object, the following strategy can be used (Based on this blogpost, which also provides more explanation):
from dataclasses import dataclass, field@dataclassclass Test: name: str _name: str = field(init=False, repr=False) @property def name(self) -> str: return self._name @name.setter def name(self, name: str) -> None: self._name = name
This can now be used as one would expect from a dataclass with a data member name
:
my_test = Test(name='foo')my_test.name = 'bar'my_test.name('foobar')print(my_test.name)
The above implementation does the following things:
- The
name
class member will be used as the public interface, but it actually does not really store anything - The
_name
class member stores the actual content. The assignment withfield(init=False, repr=False)
makes sure that the@dataclass
decorator ignores it when constructing the__init__()
and__repr__()
methods. - The getter/setter for
name
actually returns/sets the content of_name
- The initializer generated through the
@dataclass
will use the setter that we just defined. It will not initialize_name
explicitly, because we told it not to do so.