简体   繁体   中英

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.

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.

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 ")

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM