2

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):
        ...

    @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.

Subhayan Bhattacharya
  • 4,615
  • 4
  • 35
  • 56
  • 1
    I will repeat my comment from your [latter question](https://stackoverflow.com/q/59222092/2750819). See https://stackoverflow.com/q/57791679/2750819: while the answer primarily deals with `__init__`, the same applies for `__post_init__` (as in the CPython implemenation, internally `__init__` calls `__post_init__`). – Kent Shikama Dec 07 '19 at 22:40

1 Answers1

2

You can do what the frozen initialisator in dataclasses itself does and use object.__setattr__ to assign values:

...
    def __post_init__(self):
        object.__setattr__(self, 'sort_index', 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)
...

Which, given your testcase, returns

Jack and 35
Jason and 45
James and 60

for me.

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.

Arne
  • 13,772
  • 4
  • 65
  • 83