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

2

4

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.

Ozoniferous answered 6/12, 2019 at 23:56 Comment(1)
I will repeat my comment from your latter question. See https://mcmap.net/q/219841/-using-dataclasses-with-dependent-attributes-via-property/2750819: while the answer primarily deals with __init__, the same applies for __post_init__ (as in the CPython implemenation, internally __init__ calls __post_init__).Wolsey
F
3

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.

Frankfurter answered 9/12, 2019 at 12:48 Comment(5)
This doesn't seem to work, assigning to the property setter just raises FrozenInstanceError: cannot assign to field 'age'Welladvised
Still works for me. I fixed up my sample code so that it can copy-pasted more easily, I guess something got mixed up while applying the fix to the initial question's code.Frankfurter
Like Hubro, a=Person(name="Jack", lastname="Ryan", age=35); a.age=55 will give me dataclasses.FrozenInstanceError: cannot assign to field 'age'. Seems like frozen dataclasses are really frozen after all...Mauri
@Mauri which version of python are you on?Frankfurter
@Frankfurter My Python version is 3.12.4Mauri
D
0

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 ")
Drawn answered 2/2, 2023 at 10:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.