Dataclasses and property decorator Dataclasses and property decorator python python

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 with field(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.