Классы данных и декоратор свойств

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


person GertVdE    schedule 28.06.2018    source источник
comment
florimond.dev/blog/articles/2018 / 10 /   -  person demberto    schedule 05.02.2021


Ответы (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.

person shmee    schedule 28.06.2018
comment
Я думаю, мне бы хотелось, чтобы классы данных позволяли свойству переопределять получение или настройку без необходимости называть поля с ведущим подчеркиванием. Частью сахара класса данных является инициализация, которая будет означать, что вы получите Test(_name='foo') - это означает, что ваш интерфейс будет отличаться от вашего творения. Это небольшая цена, но все же разница между классами данных и именованными кортежами настолько мала, что это было бы чем-то еще полезным (что отличает их больше и, следовательно, дает больше смысла). - person Marc; 05.09.2018
comment
@Marc Они делают! Используйте классические методы получения и установки и вызывайте функцию установки в init вместо прямого назначения. 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
comment
@Marc У меня было такое же беспокойство. здесь - хорошее объяснение того, как решить эту проблему. - person JorenV; 10.01.2019
comment
@JorenV спасибо за объяснение. ИМХО, это лучший способ сделать это в настоящее время. Я все еще хотел бы, чтобы классы данных даже танцевали за вас (но я могу согласиться на это) - в конце концов, это более явно. - person Marc; 10.01.2019
comment
@JorenV, Спасибо за ссылку на это объяснение. Я прочитал его и попытался реализовать сам, а затем начал задаваться вопросом, почему я испытываю все эти проблемы, когда я мог просто сохранить обычный класс вместо класса данных и избежать всего этого. - person JasonArg123; 06.06.2019
comment
@JorenV, вам следует подумать о создании ответа с вашим комментарием, так как это отличное решение, но оно несколько похоронено здесь в комментариях. - person Dan Coates; 13.04.2020
comment
@DanCoates, спасибо, что указали на это. Я только что создал правильный ответ. - person JorenV; 13.04.2020

ДВЕ ВЕРСИИ, ПОДДЕРЖИВАЮЩИЕ ЗНАЧЕНИЯ ПО УМОЛЧАНИЮ

Большинство опубликованных подходов не обеспечивают удобочитаемый способ установки значения по умолчанию для свойства, которое является довольно важной частью 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()
person Martin CR    schedule 28.04.2020
comment
Как кто-то указал в другом потоке, если вы столкнетесь с такими большими проблемами, вероятно, лучше просто использовать обычный класс ... - person Martin CR; 23.05.2020

@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 явно, потому что мы сказали ему этого не делать.
person JorenV    schedule 13.04.2020
comment
Это лучший ответ IMHO, но ему не хватает (важной) возможности устанавливать значения по умолчанию для свойств, которые не указаны при создании экземпляра класса. См. Мой ответ, чтобы узнать, как это разрешить. - person Martin CR; 28.04.2020
comment
Обратите внимание, что mypy будет жаловаться на двойное определение name! Однако ошибок времени выполнения нет. - person gmagno; 08.08.2020
comment
FWIW, я добавил подход с метаклассами, который помогает поддерживать свойства со значениями по умолчанию - person rv.kvetch; 24.07.2021

Вот что я сделал, чтобы определить поле как свойство в __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 в качестве аргумента вместо поля по умолчанию.

person Samsara Apathika    schedule 10.05.2020

Некоторая упаковка может быть хорошей:

#         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
person InQβ    schedule 25.11.2019

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

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}).

person Conchylicultor    schedule 08.10.2019
comment
Для вашего первого подхода кажется также возможным сделать его наизнанку. Определение _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")
person teebr    schedule 28.02.2021
comment
Это довольно солидное решение. Вы избавляетесь от необходимости использовать метод свойств, который может быть как плюсом, так и минусом. Лично мне нравится концепция свойств, потому что я чувствую, что это действительно Pythonic, но я все же пошел дальше и поддержал, поскольку это определенно правильный подход. - person rv.kvetch; 22.07.2021

Вот еще один способ, позволяющий использовать поля без подчеркивания в начале:

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')
person PaperNick    schedule 03.12.2020
comment
Единственная проблема с этим подходом (по крайней мере, о которой я знаю) заключается в том, что PyCharm жалуется при доступе или чтении свойства. Например: 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
person Remolten    schedule 04.02.2021
comment
Кажется, что среда IDE жалуется, если вызывает конструктор без аргументов, поэтому я бы, вероятно, предложил определить его как 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
person bluesummers    schedule 17.02.2020
comment
Вам не нужно и не нужно создавать атрибут экземпляра с именем 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

person MikeSchneeberger    schedule 20.01.2020

Попробовав разные предложения из этой ветки, я получил немного измененную версию ответа @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}
person Roman Zhuravlev    schedule 17.11.2020

Хорошо, это моя первая попытка сделать все самодостаточным в классе.

Я попробовал несколько разных подходов, в том числе наличие декоратора класса рядом с @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
person rv.kvetch    schedule 22.07.2021
comment
Несколько более упрощенное использование первого примера: gist.github.com/rnag/e8c641d7fed3e2967dd - person rv.kvetch; 23.07.2021