← Назад к вопросам

Что такое property декоратор и когда его использовать?

2.0 Middle🔥 171 комментариев
#DevOps и инфраструктура#Django

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое property декоратор и когда его использовать?

Основная идея

@property — это встроенный декоратор Python, который превращает метод в атрибут-свойство. Это позволяет вызывать метод как обычный атрибут, без круглых скобок, но при этом выполняется дополнительная логика.

Это реализация паттерна getter/setter без явного вызова методов:

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    # Без @property
    def get_name(self):
        return self._name
    
    # С @property — более элегантно
    @property
    def name(self):
        return self._name

person = Person("Alice", 30)

# Без @property нужны скобки
name1 = person.get_name()  # Alice

# С @property — как обычный атрибут
name2 = person.name  # Alice

Синтаксис и основные декораторы

Комплект из трёх декораторов:

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    # Getter — чтение свойства
    @property
    def width(self):
        print("Получаем ширину...")
        return self._width
    
    # Setter — установка значения
    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("Ширина должна быть положительной")
        print(f"Устанавливаем ширину: {value}")
        self._width = value
    
    # Deleter — удаление свойства
    @width.deleter
    def width(self):
        print("Удаляем ширину...")
        del self._width

# Использование
rect = Rectangle(10, 20)
print(rect.width)  # Получаем ширину... → 10

rect.width = 15  # Устанавливаем ширину: 15
print(rect.width)  # Получаем ширину... → 15

rect.width = -5  # ValueError: Ширина должна быть положительной

del rect.width  # Удаляем ширину...

Практический пример: Валидация

Основное применение — валидация данных при присваивании:

class User:
    def __init__(self, email):
        self._email = None
        self.email = email  # Использует setter с валидацией
    
    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, value):
        # Валидация перед сохранением
        if "@" not in value:
            raise ValueError("Некорректный email")
        self._email = value
    
    @email.deleter
    def email(self):
        self._email = None

user = User("test@example.com")
print(user.email)  # test@example.com

user.email = "invalid-email"  # ValueError: Некорректный email

Вычисляемые свойства

@property часто используется для вычисляемых атрибутов, которые зависят от других данных:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    # Вычисляемое свойство — нет _area, вычисляется на лету
    @property
    def area(self):
        return self.width * self.height
    
    @property
    def perimeter(self):
        return 2 * (self.width + self.height)

rect = Rectangle(10, 20)
print(rect.area)       # 200
print(rect.perimeter)  # 60

# При изменении размеров — свойства автоматически пересчитываются
rect.width = 5
print(rect.area)  # 100

Кэширование с @property (lru_cache)

Для дорогостоящих вычислений можно кэшировать результат:

from functools import lru_cache, cached_property

class DataProcessor:
    def __init__(self, data):
        self.data = data
    
    # Вариант 1: property с lru_cache (старый способ)
    @property
    @lru_cache(maxsize=1)
    def expensive_result(self):
        print("Выполняем дорогостоящее вычисление...")
        return sum(self.data) * 2
    
    # Вариант 2: cached_property (новый способ, Python 3.8+)
    @cached_property
    def another_result(self):
        print("Вычисляем...")
        return len(self.data) ** 2

processor = DataProcessor([1, 2, 3, 4, 5])
print(processor.expensive_result)  # Вычисляем... → 30
print(processor.expensive_result)  # Возвращаем кэш → 30

Инкапсуляция: Защита данных

@property помогает скрыть внутреннюю реализацию:

class BankAccount:
    def __init__(self, balance):
        self._balance = balance  # Внутренний атрибут (защищён)
    
    @property
    def balance(self):
        return self._balance
    
    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
        else:
            raise ValueError("Сумма должна быть положительной")
    
    def withdraw(self, amount):
        if amount > self._balance:
            raise ValueError("Недостаточно средств")
        self._balance -= amount

account = BankAccount(1000)
print(account.balance)  # 1000

# Прямое изменение невозможно (нет setter)
account.balance = 500  # AttributeError

# Правильный способ
account.deposit(100)
print(account.balance)  # 1100

Сравнение подходов

# Плохо — открытый атрибут
class PersonBad:
    def __init__(self, age):
        self.age = age

person = PersonBad(25)
person.age = -5  # Нет валидации!


# Хорошо — с @property
class PersonGood:
    def __init__(self, age):
        self._age = None
        self.age = age  # Использует setter
    
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        if not 0 <= value <= 150:
            raise ValueError("Некорректный возраст")
        self._age = value

person = PersonGood(25)
print(person.age)  # 25
person.age = -5    # ValueError

Когда использовать @property

ИСПОЛЬЗУЙ когда:

  1. Валидация данных при присваивании
  2. Вычисляемые атрибуты (зависят от других полей)
  3. Инкапсуляция (скрытие внутренней реализации)
  4. Логирование доступа к атрибутам
  5. Кэширование дорогостоящих вычислений

НЕ используй когда:

  1. Нет валидации или логики — просто переди открытый атрибут
  2. Операция дорогостоящая, но не кэшируется — методом лучше
  3. Множественные побочные эффекты — явный метод лучше

Альтернативы: getattr и setattr

class DynamicProperties:
    def __init__(self):
        self._data = {}
    
    def __getattr__(self, name):
        if name in self._data:
            return self._data[name]
        raise AttributeError(f"Атрибут {name} не найден")
    
    def __setattr__(self, name, value):
        if name.startswith("_"):
            super().__setattr__(name, value)
        else:
            self._data[name] = value

obj = DynamicProperties()
obj.foo = 10
print(obj.foo)  # 10

Лучшие практики

class Animal:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    @property
    def name(self):
        """Возвращает имя животного."""
        return self._name
    
    @property
    def age(self):
        """Возвращает возраст в годах."""
        return self._age
    
    @age.setter
    def age(self, value):
        """Устанавливает возраст с валидацией."""
        if not isinstance(value, int) or value < 0:
            raise ValueError("Возраст должен быть неотрицательным целым числом")
        self._age = value
    
    @property
    def is_adult(self):
        """Проверяет, является ли животное взрослым (возраст >= 2)."""
        return self._age >= 2

Заключение

@property — это мощный инструмент для валидации данных, создания вычисляемых атрибутов, инкапсуляции и защиты данных. Он делает код более Pythonic, скрывая сложность за простым интерфейсом атрибутов.

Что такое property декоратор и когда его использовать? | PrepBro