Python decorator and @property

Вы используете свойства (@property) в классах?
Удобная штука, скажу я вам! Но работают они только для инстансов класса. Вот простой пример:

class A:
@property
def prop(self):
return 10

Создаём класс и получаем значение свойства

>>> a = A()
>>> a.prop
10

А что будет если вызвать свойство у класса

>>> A.prop
<property object at 0x000…318>Как сделать подобие property для класса?
Есть несколько способов:

  • Написать свой декоратор в точности повторяющий функционал property. 
  • Если вы уже перешли на Python 3.9 то можете написать вот так:
  • class С:
    @classmethod
    @property
    def x(cls):
    return 2+2

>>> C.x
4

Например, вот так можно динамический докстринг сделать

class C:
@classmethod
@property
def __doc__(cls) -> str:
return f”A doc for {cls.name!r}”

>>> help(C)
class C(builtins.object)
| A doc for ‘C’

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

class C:
_x = 0
def x_get(cls):
return cls._x
def x_set(cls, value):
cls._x = value
x = classmethod(property(x_get, x_set))

Выражение присвоения просто перезаписывает атрибут x, а не вызывает setter.

>>> C.x = 1
>>> C._x
0

Чтобы setter тоже работал, нужно сделать иначе.

Теперь мы хотим иметь рабочий setter.

🔸 Вместо класса создаем свойства для его мета-класса

class CMeta(type):
_x = 0
@property
def x(cls):
return cls._x
@x.setter
def x(cls, value):
cls._x = value

class C(metaclass=CMeta):
_x = 2

При этом можем изменить дефолтное значение для унаследованного класса. Плюс, как и в обычном property можно сделать getter, setter и deleter.

>>> C.x
2
>>> C.x = 34
>>> C.x
34

Проверим, действительно ли сработал setter или мы просто перезаписали атрибут x

>>> C._x
34

Да, всё верно! 😎

🔸 Динамический атрибут класса. Похож на прошлый пример но с дополнительной фишкой.
Что он делает? Этот декоратор не только позволяет добавить свойство класса но и разделить функционал для свойства класса и свойства инстанса.

Работает это через дополнительный вызов __getattr__ и __setattr__ мета-класса, где требуется проверить имя атрибута и сделать соответствующие выводы.

from types import DynamicClassAttribute

class CMeta(type):
def __getattr__(self, item):
if item == ‘x’: # проверка имени
return ‘x from class’
raise AttributeError
def __setattr__(self, key, value):
print(‘set class’, key, ‘=’, value)

class C(metaclass=CMeta):
@DynamicClassAttribute
def x(self):
return ‘x from instance’
@x.setter
def x(self, value):
print(‘set instance x =’, value)

>>> C.x
‘from class’
>>> C().x
‘from instance’
>>> C.x = 2
‘set class x = 2’
C().x = 2
‘set instance x = 2’

Обсуждение закрыто.