@property используется для создания вычисляемых атрибутов класса.  Вместо хранения значения, атрибут вычисляется "на лету" при обращении к нему. Можно комбинировать с другими декораторами, например, @lru_cache для мемоизации результатов вычислений.
  
from functools import lru_cache
class Circle:
    def __init__(self, radius):
        self._radius = radius
    @property
    def radius(self):
        return self._radius
    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value
    @property
    @lru_cache(maxsize=None)
    def area(self):
        print("Calculating area...")
        return 3.14159 * self._radius ** 2
  area - вычисляемый атрибут, который мемоизирован. Setter для radius используется для контроля изменения радиуса.
    Использование @property с декораторами позволяет создавать вычисляемые атрибуты, которые выглядят и используются как обычные атрибуты, но их значения генерируются "на лету" при обращении к ним. Это мощный механизм для инкапсуляции логики вычислений и поддержания консистентности данных.
  
    Основная идея состоит в том, что @property определяет метод, который будет вызываться при попытке получить значение атрибута. Дополнительно, можно использовать @ и @ для определения методов, которые будут вызываться при попытке установить или удалить значение атрибута соответственно.  В случае вычисляемых атрибутов, setter и deleter обычно не используются, потому что атрибут вычисляется и не должен устанавливаться напрямую.
  
    Пример использования @property с декораторами для вычисляемого атрибута:
  
class Circle:
    def __init__(self, radius):
        self._radius = radius  # Используем _radius, чтобы избежать конфликта имен
    @property
    def radius(self):
        """Возвращает радиус круга."""
        return self._radius
    @radius.setter
    def radius(self, value):
        """Устанавливает радиус круга. Проверяет, что радиус положительный."""
        if value <= 0:
            raise ValueError("Радиус должен быть положительным")
        self._radius = value
    @property
    def area(self):
        """Вычисляет и возвращает площадь круга."""
        return 3.14159 * self._radius * self._radius
    @property
    def diameter(self):
        """Вычисляет и возвращает диаметр круга."""
        return 2 * self._radius
В этом примере:
radius - обычный атрибут с getter и setter.area - вычисляемый атрибут, который возвращает площадь круга, основанную на текущем радиусе.diameter - вычисляемый атрибут, возвращающий диаметр.Ключевые моменты:
@property преобразует метод area в атрибут, к которому можно обращаться как к circle.area, без необходимости вызова метода как circle.area().area вычисляется только при обращении к нему. Если радиус изменится, новое значение площади будет вычислено при следующем обращении к circle.area.@radius.setter позволяет контролировать процесс присвоения нового значения радиусу, добавляя валидацию.  Это важно для поддержания целостности данных.setter не определяют, если не требуется какой-то хитрый механизм обновления связанных данных.
    Преимущества использования @property для вычисляемых атрибутов:
  
    В заключение, @property с декораторами – это мощный и элегантный способ реализации вычисляемых атрибутов в Python, позволяющий создавать более чистый, поддерживаемый и расширяемый код.