Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can you use property setter when using frozen dataclasses in Python

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.

like image 906
Subhayan Bhattacharya Avatar asked Oct 15 '25 14:10

Subhayan Bhattacharya


1 Answers

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.

like image 56
Arne Avatar answered Oct 18 '25 12:10

Arne



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!