Reputation: 5723
I was just playing around with the concept of Python dataclasses and abstract classes and what i am trying to achieve is basically create a frozen dataclass but at the same time have one attribute as a property. Below is my code for doing so:
import abc
from dataclasses import dataclass, field
class AbsPersonModel(metaclass=abc.ABCMeta):
@property
@abc.abstractmethod
def age(self):
...
@age.setter
@abc.abstractmethod
def age(self, value):
...
@abc.abstractmethod
def multiply_age(self, factor):
...
@dataclass(order=True, frozen=True)
class Person(AbsPersonModel):
sort_index: int = field(init=False, repr=False)
name: str
lastname: str
age: int
_age: int = field(init=False, repr=False)
def __post_init__(self):
self.sort_index = self.age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0 or value > 100:
raise ValueError("Non sensical age cannot be set!")
self._age = value
def multiply_age(self, factor):
return self._age * factor
if __name__ == "__main__":
persons = [
Person(name="Jack", lastname="Ryan", age=35),
Person(name="Jason", lastname="Bourne", age=45),
Person(name="James", lastname="Bond", age=60)
]
sorted_persons = sorted(persons)
for person in sorted_persons:
print(f"{person.name} and {person.age}")
When i run this i get the below error:
Traceback (most recent call last):
File "abstract_prac.py", line 57, in <module>
Person(name="Jack", lastname="Ryan", age=35),
File "<string>", line 4, in __init__
File "abstract_prac.py", line 48, in age
self._age = value
File "<string>", line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field '_age'
How can i get the best of both worlds(dataclasses and also using property along with it)?
Any help would be much appreciated.
Upvotes: 4
Views: 4408
Reputation: 7725
I know maybe this answer is more generic, but it could help as it's more simple than the other answer, and doesn't use any getter or setter
Simply use __post_init__
import random
from dataclasses import dataclass, FrozenInstanceError
@dataclass(repr=True, eq=True, order=False, unsafe_hash=False, frozen=True)
class Person:
name: str
age: int = None
def __post_init__(self):
object.__setattr__(self, 'age', self.calc_age())
@staticmethod
def calc_age():
return random.randint(0, 100)
if __name__ == '__main__':
person = Person("Fede")
person2 = Person("Another")
print(person)
print(person2)
try:
person.age = 1
except FrozenInstanceError:
print("can't set age ")
Upvotes: 0
Reputation: 20237
You can do what the frozen
initialisator in dataclasses itself does and use object.__setattr__
to assign values. Given your abstract class, this dataclass definition should work:
@dataclass(order=True, frozen=True)
class Person:
sort_index: int = field(init=False, repr=False)
name: str
lastname: str
age: int
_age: int = field(init=False, repr=False) # can actually be omitted
def __post_init__(self):
object.__setattr__(self, 'sort_index', self.age)
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0 or value > 100:
raise ValueError("Non sensical age cannot be set!")
object.__setattr__(self, '_age', value)
def multiply_age(self, factor):
return self._age * factor
Running your test suite should now return the expected
Jack and 35
Jason and 45
James and 60
This works because setting a dataclass to frozen
disables that class' own __setattr__
and makes it just raise the exception you saw. Any __setattr__
of its superclasses (which always includes object
) will still work.
Upvotes: 3