Subhayan Bhattacharya
Subhayan Bhattacharya

Reputation: 5723

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.

Upvotes: 4

Views: 4408

Answers (2)

Federico Ba&#249;
Federico Ba&#249;

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

Arne
Arne

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

Related Questions