Python Dataclass и декоратор свойств

Я читал на Python 3.7 dataclass в качестве альтернативы namedtuples (что я обычно использую при группировании данных в структуре). Мне было интересно, совместим ли dataclass с декоратором свойств, чтобы определить функции getter и setter для элементов данных в dataclass. Если да, то это описано где-то? Или есть примеры?

Ответ 1

Он уверен, что работает:

from dataclasses import dataclass

@dataclass
class Test:
    _name: str="schbell"

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, v: str) -> None:
        self._name = v

t = Test()
print(t.name) # schbell
t.name = "flirp"
print(t.name) # flirp
print(t) # Test(_name='flirp')

На самом деле, почему это не так? В конце концов, то, что вы получаете, это просто старый добрый класс, полученный от типа:

print(type(t)) # <class '__main__.Test'>
print(type(Test)) # <class 'type'>

Возможно, именно поэтому свойства нигде не упоминаются конкретно. Однако в PEP-557 Abstract упоминается общая пригодность известных функций класса Python:

Поскольку классы данных используют стандартный синтаксис определения классов, вы можете использовать наследование, метаклассы, docstrings, определяемые пользователем методы, фабрики классов и другие функции класса Python.

Ответ 2

В настоящее время лучший способ, который я нашел, - создать отдельный класс данных, содержащий свойство.

from dataclasses import dataclass, field

@dataclass
class _A:
    x: int = 0
    y: int = 1

class A(_A):
    @property
    def x(self) -> int:
        return self._x

    @x.setter
    def x(self, value: int):
        self._x = value

Класс ведет себя как обычный класс данных. И правильно определит поля __repr__ и __init__ (Vehicle(wheels=4) вместо Vehicle(_wheels=4). Недостатком является то, что свойства не могут быть доступны только для чтения.

Этот пост пытается перезаписать атрибут dataclass wheel с помощью property с тем же именем. Однако @property перезаписывает значение по умолчанию field, что приводит к неожиданному поведению.

from dataclasses import dataclass, field

@dataclass
class A:

    x: int

    # same as: 'x = property(x)  # Overwrite any field() info'
    @property
    def x(self) -> int:
        return self._x

    @x.setter
    def x(self, value: int):
        self._x = value

A()  # 'A(x=<property object at 0x7f0cf64e5fb0>)'   Oups

print(A.__dataclass_fields__)  # {'x': Field(name='x',type=<class 'int'>,default=<property object at 0x>,init=True,repr=True}

Одним из способов решения этой проблемы, избегая наследования, было бы перезапись поля вне определения класса после вызова метакласса класса данных.

@dataclass
class A:
  x: int

def x_getter(self):
  return self._x

def x_setter(self, value):
  self._x = value

A.x = property(x_getter)
A.x = A.x.setter(x_setter)

print(A(x=1))
print(A())  # missing 1 required positional argument: 'x'

Вероятно, можно будет перезаписать это автоматически, создав некоторый пользовательский метакласс и установив несколько field(metadata={'setter': _x_setter, 'getter': _x_getter}).