Я читал о классе данных Python 3.7 в качестве альтернативы именованным кортежам (что я обычно использую при группировке данных в структуре). Мне было интересно, совместим ли класс данных с декоратором свойств для определения функций получения и установки для элементов данных класса данных. Если да, то это где-то описано? Или есть примеры?
Классы данных и декоратор свойств
Ответы (13)
Это действительно работает:
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 упоминается общее удобство использования well- известные особенности класса Python:
Поскольку классы данных используют обычный синтаксис определения классов, вы можете свободно использовать наследование, метаклассы, строки документации, определяемые пользователем методы, фабрики классов и другие функции классов Python.
Test(_name='foo')
- это означает, что ваш интерфейс будет отличаться от вашего творения. Это небольшая цена, но все же разница между классами данных и именованными кортежами настолько мала, что это было бы чем-то еще полезным (что отличает их больше и, следовательно, дает больше смысла).
- person Marc; 05.09.2018
def set_booking_ref(self, value:str): self._booking_ref = value.strip()
... booking_ref = property(get_booking_ref, set_booking_ref)
... def __init__(self, booking_ref :str): self.set_booking_ref(self, booking_ref)
. Не знаю, как это сделать с @property
декоратором.
- person Alan; 21.09.2018
ДВЕ ВЕРСИИ, ПОДДЕРЖИВАЮЩИЕ ЗНАЧЕНИЯ ПО УМОЛЧАНИЮ
Большинство опубликованных подходов не обеспечивают удобочитаемый способ установки значения по умолчанию для свойства, которое является довольно важной частью dataclass. Вот два возможных способа сделать это.
Первый способ основан на подходе, на который ссылается @JorenV. Он определяет значение по умолчанию в _name = field()
и использует наблюдение, что если начальное значение не указано, то установщику передается сам объект property:
from dataclasses import dataclass, field
@dataclass
class Test:
name: str
_name: str = field(init=False, repr=False, default='baz')
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
if type(value) is property:
# initial value not specified, use default
value = Test._name
self._name = value
def main():
obj = Test(name='foo')
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'bar'
print(obj) # displays: Test(name='bar')
obj = Test()
print(obj) # displays: Test(name='baz')
if __name__ == '__main__':
main()
Второй способ основан на том же подходе, что и @Conchylicultor: обход механизма dataclass путем перезаписи поля вне определения класса.
Лично я считаю, что этот способ чище и удобнее для чтения, чем первый, потому что он следует обычной идиоме dataclass для определения значения по умолчанию и не требует «магии» в установщике.
Даже в этом случае я бы предпочел, чтобы все было автономным ... возможно, какой-нибудь умный человек найдет способ включить обновление поля в dataclass.__post_init__()
или что-то подобное?
from dataclasses import dataclass
@dataclass
class Test:
name: str = 'foo'
@property
def _name(self):
return self._my_str_rev[::-1]
@_name.setter
def _name(self, value):
self._my_str_rev = value[::-1]
# --- has to be called at module level ---
Test.name = Test._name
def main():
obj = Test()
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'baz'
print(obj) # displays: Test(name='baz')
obj = Test(name='bar')
print(obj) # displays: Test(name='bar')
if __name__ == '__main__':
main()
@property
обычно используется для хранения, казалось бы, общедоступного аргумента (например, name
) в частном атрибуте (например, _name
) через геттеры и сеттеры, в то время как классы данных генерируют для вас метод __init__()
. Проблема в том, что этот сгенерированный __init__()
метод должен взаимодействовать через публичный аргумент name
, при этом внутренне устанавливая частный атрибут _name
. Классы данных не делают этого автоматически.
Чтобы иметь один и тот же интерфейс (через name
) для установки значений и создания объекта, можно использовать следующую стратегию (на основе этот блог, который также предоставляет дополнительные объяснения):
from dataclasses import dataclass, field
@dataclass
class Test:
name: str
_name: str = field(init=False, repr=False)
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
Теперь это можно использовать, как и следовало ожидать от класса данных с элементом данных name
:
my_test = Test(name='foo')
my_test.name = 'bar'
my_test.name('foobar')
print(my_test.name)
Вышеупомянутая реализация выполняет следующие действия:
- Член класса
name
будет использоваться как общедоступный интерфейс, но на самом деле он ничего не хранит. - Член класса
_name
хранит фактическое содержимое. Назначениеfield(init=False, repr=False)
гарантирует, что декоратор@dataclass
проигнорирует его при создании методов__init__()
и__repr__()
. - Получатель / установщик для
name
фактически возвращает / устанавливает содержимое_name
- Инициализатор, сгенерированный через
@dataclass
, будет использовать только что определенный установщик. Он не будет инициализировать_name
явно, потому что мы сказали ему этого не делать.
name
! Однако ошибок времени выполнения нет.
- person gmagno; 08.08.2020
Вот что я сделал, чтобы определить поле как свойство в __post_init__
. Это полный взлом, но он работает с dataclasses
инициализацией на основе dict и даже с классами marshmallow_dataclass.
from dataclasses import dataclass, field, asdict
@dataclass
class Test:
name: str = "schbell"
_name: str = field(init=False, repr=False)
def __post_init__(self):
# Just so that we don't create the property a second time.
if not isinstance(getattr(Test, "name", False), property):
self._name = self.name
Test.name = property(Test._get_name, Test._set_name)
def _get_name(self):
return self._name
def _set_name(self, val):
self._name = val
if __name__ == "__main__":
t1 = Test()
print(t1)
print(t1.name)
t1.name = "not-schbell"
print(asdict(t1))
t2 = Test("llebhcs")
print(t2)
print(t2.name)
print(asdict(t2))
Это напечатает:
Test(name='schbell')
schbell
{'name': 'not-schbell', '_name': 'not-schbell'}
Test(name='llebhcs')
llebhcs
{'name': 'llebhcs', '_name': 'llebhcs'}
На самом деле я начал с этого сообщения в блоге упоминается где-то в этом SO, но столкнулся с проблемой, что поле dataclass было установлено на тип property
, потому что декоратор применяется к классу. Это,
@dataclass
class Test:
name: str = field(default='something')
_name: str = field(init=False, repr=False)
@property
def name():
return self._name
@name.setter
def name(self, val):
self._name = val
сделает name
типом property
, а не str
. Таким образом, установщик фактически получит объект property
в качестве аргумента вместо поля по умолчанию.
Некоторая упаковка может быть хорошей:
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# Version 2, December 2004
#
# Copyright (C) 2020 Xu Siyuan <[email protected]>
#
# Everyone is permitted to copy and distribute verbatim or modified
# copies of this license document, and changing it is allowed as long
# as the name is changed.
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
# 0. You just DO WHAT THE FUCK YOU WANT TO.
from dataclasses import dataclass, field
MISSING = object()
__all__ = ['property_field', 'property_dataclass']
class property_field:
def __init__(self, fget=None, fset=None, fdel=None, doc=None, **kwargs):
self.field = field(**kwargs)
self.property = property(fget, fset, fdel, doc)
def getter(self, fget):
self.property = self.property.getter(fget)
return self
def setter(self, fset):
self.property = self.property.setter(fset)
return self
def deleter(self, fdel):
self.property = self.property.deleter(fdel)
return self
def property_dataclass(cls=MISSING, / , **kwargs):
if cls is MISSING:
return lambda cls: property_dataclass(cls, **kwargs)
remembers = {}
for k in dir(cls):
if isinstance(getattr(cls, k), property_field):
remembers[k] = getattr(cls, k).property
setattr(cls, k, getattr(cls, k).field)
result = dataclass(**kwargs)(cls)
for k, p in remembers.items():
setattr(result, k, p)
return result
Вы можете использовать это так:
@property_dataclass
class B:
x: int = property_field(default_factory=int)
@x.getter
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
На данный момент лучший способ, который я нашел, - это перезапись полей класса данных по свойству в отдельном дочернем классе.
from dataclasses import dataclass, field
@dataclass
class _A:
x: int = 0
class A(_A):
@property
def x(self) -> int:
return self._x
@x.setter
def x(self, value: int):
self._x = value
Класс ведет себя как обычный класс данных. И будет правильно определять поля __repr__
и __init__
(A(x=4)
вместо A(_x=4)
. Недостатком является то, что свойства не могут быть доступны только для чтения.
В этом сообщении в блоге делается попытка перезаписать атрибут класса данных колес на 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})
.
_A
с помощью геттера и сеттера, а @dataclass
внешнего A(_A)
.
- person InQβ; 25.11.2019
Решение с минимальным дополнительным кодом и без скрытых переменных состоит в том, чтобы переопределить метод __setattr__
для выполнения любых проверок в поле:
@dataclass
class Test:
x: int = 1
def __setattr__(self, prop, val):
if prop == "x":
self._check_x(val)
super().__setattr__(prop, val)
@staticmethod
def _check_x(x):
if x <= 0:
raise ValueError("x must be greater than or equal to zero")
Вот еще один способ, позволяющий использовать поля без подчеркивания в начале:
from dataclasses import dataclass
@dataclass
class Person:
name: str = property
@name
def name(self) -> str:
return self._name
@name.setter
def name(self, value) -> None:
self._name = value
def __post_init__(self) -> None:
if isinstance(self.name, property):
self.name = 'Default'
Результат:
print(Person().name) # Prints: 'Default'
print(Person('Joel').name) # Prints: 'Joel'
print(repr(Person('Jane'))) # Prints: Person(name='Jane')
print(p.name)
assert p.name == 'test'
. Я предполагаю, что обходным путем можно было бы назначить его как name: str = None
и украсить самим @property
; PyCharm по-прежнему жалуется на уровне реализации, но на стороне клиента предупреждения теперь, кажется, исчезают.
- person rv.kvetch; 27.07.2021
Этот метод использования свойств в классах данных также работает с asdict
и к тому же проще. Почему? Поля, набранные с помощью ClassVar
, игнорируются классом данных, но мы все равно можем использовать их в наших свойствах.
@dataclass
def SomeData:
uid: str
_uid: ClassVar[str]
@property
def uid(self) -> str:
return self._uid
@uid.setter
def uid(self, uid: str) -> None:
self._uid = uid
uid: str = None
. Конечно, еще одна проблема заключается в том, что uid
устанавливается для объекта свойства, если через конструктор не указано значение, но это можно легко решить, например, с помощью декоратора.
- person rv.kvetch; 22.07.2021
После очень подробного сообщения о классах данных и свойствах, которые можно найти здесь версия TL; DR, которая решает некоторые очень уродливые случаи, когда вам нужно вызывать MyClass(_my_var=2)
и странные __repr__
выходы:
from dataclasses import field, dataclass
@dataclass
class Vehicle:
wheels: int
_wheels: int = field(init=False, repr=False)
def __init__(self, wheels: int):
self._wheels = wheels
@property
def wheels(self) -> int:
return self._wheels
@wheels.setter
def wheels(self, wheels: int):
self._wheels = wheels
wheels
. Если вы хотите, чтобы __init__
инициализировал _wheels
через установщик, используйте wheels = InitVar[int]
, затем используйте __post_init__
для установки self.wheels = wheels
.
- person chepner; 23.03.2020
Исходя из приведенных выше идей, я создал функцию-декоратор классов resolve_abc_prop
, которая создает новый класс, содержащий функции получения и установки, как это было предложено @shmee.
def resolve_abc_prop(cls):
def gen_abstract_properties():
""" search for abstract properties in super classes """
for class_obj in cls.__mro__:
for key, value in class_obj.__dict__.items():
if isinstance(value, property) and value.__isabstractmethod__:
yield key, value
abstract_prop = dict(gen_abstract_properties())
def gen_get_set_properties():
""" for each matching data and abstract property pair,
create a getter and setter method """
for class_obj in cls.__mro__:
if '__dataclass_fields__' in class_obj.__dict__:
for key, value in class_obj.__dict__['__dataclass_fields__'].items():
if key in abstract_prop:
def get_func(self, key=key):
return getattr(self, f'__{key}')
def set_func(self, val, key=key):
return setattr(self, f'__{key}', val)
yield key, property(get_func, set_func)
get_set_properties = dict(gen_get_set_properties())
new_cls = type(
cls.__name__,
cls.__mro__,
{**cls.__dict__, **get_set_properties},
)
return new_cls
Здесь мы определяем класс данных AData
и миксин AOpMixin
, реализующий операции с данными.
from dataclasses import dataclass, field, replace
from abc import ABC, abstractmethod
class AOpMixin(ABC):
@property
@abstractmethod
def x(self) -> int:
...
def __add__(self, val):
return replace(self, x=self.x + val)
Наконец, декоратор resolve_abc_prop
затем используется для создания нового класса с данными из AData
и операциями из AOpMixin
.
@resolve_abc_prop
@dataclass
class A(AOpMixin):
x: int
A(x=4) + 2 # A(x=6)
РЕДАКТИРОВАТЬ №1: я создал пакет python, который позволяет перезаписывать абстрактные свойства с помощью класса данных: dataclass-abc
Попробовав разные предложения из этой ветки, я получил немного измененную версию ответа @Samsara Apathika. Вкратце: я удалил переменную поля подчеркивания из __init__
(поэтому он доступен для внутреннего использования, но не виден asdict()
или __dataclass_fields__
).
from dataclasses import dataclass, InitVar, field, asdict
@dataclass
class D:
a: float = 10. # Normal attribut with a default value
b: InitVar[float] = 20. # init-only attribute with a default value
c: float = field(init=False) # an attribute that will be defined in __post_init__
def __post_init__(self, b):
if not isinstance(getattr(D, "a", False), property):
print('setting `a` to property')
self._a = self.a
D.a = property(D._get_a, D._set_a)
print('setting `c`')
self.c = self.a + b
self.d = 50.
def _get_a(self):
print('in the getter')
return self._a
def _set_a(self, val):
print('in the setter')
self._a = val
if __name__ == "__main__":
d1 = D()
print(asdict(d1))
print('\n')
d2 = D()
print(asdict(d2))
Дает:
setting `a` to property
setting `c`
in the getter
in the getter
{'a': 10.0, 'c': 30.0}
in the setter
setting `c`
in the getter
in the getter
{'a': 10.0, 'c': 30.0}
Хорошо, это моя первая попытка сделать все самодостаточным в классе.
Я попробовал несколько разных подходов, в том числе наличие декоратора класса рядом с @dataclass
над определением класса. Проблема с версией декоратора заключается в том, что моя IDE жалуется, если я решаю ее использовать, а затем я теряю большинство подсказок типа, которые предоставляет декоратор dataclass
. Например, если я пытаюсь передать имя поля в метод конструктора, оно больше не будет автоматически завершаться, когда я добавляю новый декоратор класса. Я полагаю, это имеет смысл, поскольку IDE предполагает, что декоратор каким-то важным образом перезаписывает исходное определение, однако мне удалось убедить меня не пытаться использовать подход декоратора.
Я закончил добавлением метакласса для обновления свойств, связанных с полями класса данных, чтобы проверить, является ли значение, переданное в setter
, объектом свойства, как упоминалось в нескольких других решениях, и, похоже, сейчас это работает достаточно хорошо. Для тестирования должен работать любой из двух подходов, представленных ниже (на основе решения @Martin CR).
from dataclasses import dataclass, field
@dataclass
class Test(metaclass=dataclass_property_support):
name: str = property
_name: str = field(default='baz', init=False, repr=False)
@name
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
self._name = value
# --- other properties like these should not be affected ---
@property
def other_prop(self) -> str:
return self._other_prop
@other_prop.setter
def other_prop(self, value):
self._other_prop = value
А вот подход, который (неявно) сопоставляет свойство _name
, которое начинается с подчеркивания, с полем класса данных name
:
@dataclass
class Test(metaclass=dataclass_property_support):
name: str = 'baz'
@property
def _name(self) -> str:
return self._name[::-1]
@_name.setter
def _name(self, value: str):
self._name = value[::-1]
Я лично предпочитаю второй подход, потому что он, на мой взгляд, выглядит немного чище, а также поле _name
не отображается, например, при вызове вспомогательной функции класса данных asdict
.
Приведенное ниже должно работать для целей тестирования с любым из подходов, описанных выше. Самое приятное то, что моя IDE тоже не жалуется ни на один код.
def main():
obj = Test(name='foo')
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'bar'
print(obj) # displays: Test(name='bar')
obj = Test()
print(obj) # displays: Test(name='baz')
if __name__ == '__main__':
main()
Наконец, вот определение метакласса dataclass_property_support
, который, похоже, теперь работает:
from dataclasses import MISSING, Field
from functools import wraps
from typing import Dict, Any, get_type_hints
def dataclass_property_support(*args, **kwargs):
"""Adds support for using properties with default values in dataclasses."""
cls = type(*args, **kwargs)
# the args passed in to `type` will be a tuple of (name, bases, dict)
cls_dict: Dict[str, Any] = args[2]
# this accesses `__annotations__`, but should also work with sub-classes
annotations = get_type_hints(cls)
def get_default_from_annotation(field_: str):
"""Get the default value for the type annotated on a field"""
default_type = annotations.get(field_)
try:
return default_type()
except TypeError:
return None
for f, val in cls_dict.items():
if isinstance(val, property):
public_f = f.lstrip('_')
if val.fset is None:
# property is read-only, not settable
continue
if f not in annotations and public_f not in annotations:
# adding this to check if it's a regular property (not
# associated with a dataclass field)
continue
try:
# Get the value of the field named without a leading underscore
default = getattr(cls, public_f)
except AttributeError:
# The public field is probably type-annotated but not defined
# i.e. my_var: str
default = get_default_from_annotation(public_f)
else:
if isinstance(default, property):
# The public field is a property
# Check if the value of underscored field is a dataclass
# Field. If so, we can use the `default` if one is set.
f_val = getattr(cls, '_' + f, None)
if isinstance(f_val, Field) \
and f_val.default is not MISSING:
default = f_val.default
else:
default = get_default_from_annotation(public_f)
def wrapper(fset, initial_val):
"""
Wraps the property `setter` method to check if we are passed
in a property object itself, which will be true when no
initial value is specified (thanks to @Martin CR).
"""
@wraps(fset)
def new_fset(self, value):
if isinstance(value, property):
value = initial_val
fset(self, value)
return new_fset
# Wraps the `setter` for the property
val = val.setter(wrapper(val.fset, default))
# Replace the value of the field without a leading underscore
setattr(cls, public_f, val)
# Delete the property if the field name starts with an underscore
# This is technically not needed, but it supports cases where we
# define an attribute with the same name as the property, i.e.
# @property
# def _wheels(self)
# return self._wheels
if f.startswith('_'):
delattr(cls, f)
return cls