I\'ve been reading up on Python 3.7\'s dataclass as an alternative to namedtuples (what I typically use when having to group data in a structure). I was wondering if datacla
It sure does work:
from dataclasses import dataclass
@dataclass
class Test:
_name: str="schbell"
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, v: str) -> None:
self._name = v
t = Test()
print(t.name) # schbell
t.name = "flirp"
print(t.name) # flirp
print(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.
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
@dataclass
class 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:
name
class member will be used as the public interface, but it actually does not really store anything_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.name
actually returns/sets the content of _name
@dataclass
will use the setter that we just defined. It will not initialize _name
explicitly, because we told it not to do so.After trying different suggestions from this thread I've come with a little modified version of @Samsara Apathika answer. In short: I removed the "underscore" field variable from the __init__
(so it is available for internal use, but not seen by asdict()
or by __dataclass_fields__
).
from dataclasses import dataclass, InitVar, field, asdict
@dataclass
class D:
a: float = 10. # Normal attribut with a default value
b: InitVar[float] = 20. # init-only attribute with a default value
c: float = field(init=False) # an attribute that will be defined in __post_init__
def __post_init__(self, b):
if not isinstance(getattr(D, "a", False), property):
print('setting `a` to property')
self._a = self.a
D.a = property(D._get_a, D._set_a)
print('setting `c`')
self.c = self.a + b
self.d = 50.
def _get_a(self):
print('in the getter')
return self._a
def _set_a(self, val):
print('in the setter')
self._a = val
if __name__ == "__main__":
d1 = D()
print(asdict(d1))
print('\n')
d2 = D()
print(asdict(d2))
Gives:
setting `a` to property
setting `c`
in the getter
in the getter
{'a': 10.0, 'c': 30.0}
in the setter
setting `c`
in the getter
in the getter
{'a': 10.0, 'c': 30.0}
From the ideas from above, I created a class decorator function resolve_abc_prop
that creates a new class containing the getter and setter functions as suggested
by @shmee.
def resolve_abc_prop(cls):
def gen_abstract_properties():
""" search for abstract properties in super classes """
for class_obj in cls.__mro__:
for key, value in class_obj.__dict__.items():
if isinstance(value, property) and value.__isabstractmethod__:
yield key, value
abstract_prop = dict(gen_abstract_properties())
def gen_get_set_properties():
""" for each matching data and abstract property pair,
create a getter and setter method """
for class_obj in cls.__mro__:
if '__dataclass_fields__' in class_obj.__dict__:
for key, value in class_obj.__dict__['__dataclass_fields__'].items():
if key in abstract_prop:
def get_func(self, key=key):
return getattr(self, f'__{key}')
def set_func(self, val, key=key):
return setattr(self, f'__{key}', val)
yield key, property(get_func, set_func)
get_set_properties = dict(gen_get_set_properties())
new_cls = type(
cls.__name__,
cls.__mro__,
{**cls.__dict__, **get_set_properties},
)
return new_cls
Here we define a data class AData
and a mixin AOpMixin
implementing operations
on the data.
from dataclasses import dataclass, field, replace
from abc import ABC, abstractmethod
class AOpMixin(ABC):
@property
@abstractmethod
def x(self) -> int:
...
def __add__(self, val):
return replace(self, x=self.x + val)
Finally, the decorator resolve_abc_prop
is then used to create a new class
with the data from AData
and the operations from AOpMixin
.
@resolve_abc_prop
@dataclass
class A(AOpMixin):
x: int
A(x=4) + 2 # A(x=6)
EDIT #1: I created a python package that makes it possible to overwrite abstract properties with a dataclass: dataclass-abc