property в Python: проверяемое управление атрибутами
Атрибут экземпляра удобно читать напрямую, пока любое присваивание считается допустимым. Но если значение должно удовлетворять правилу, обычное поле позволяет нарушить инвариант…
Атрибут экземпляра удобно читать напрямую, пока любое присваивание считается допустимым. Но если значение должно удовлетворять правилу, обычное поле позволяет нарушить инвариант в любой точке программы. property сохраняет привычный синтаксис obj.attribute, а чтение и запись передаёт методам класса. Так класс получает единое место для проверки, вычисления или преобразования значения без обязательной замены публичного интерфейса на вызовы get_... и set_....
Как дескриптор property управляет доступом
Декоратор @property превращает метод без дополнительных аргументов в управляемый атрибут для чтения. Декоратор @name.setter связывает с тем же объектом property обработчик присваивания. Фактическое значение обычно хранят во внутреннем атрибуте вроде _temperature. Подчёркивание — соглашение о непубличной детали, а не механизм запрета доступа.
И геттер, и сеттер должны называться одинаково: именно имя связывает операции в одно свойство. Внутри сеттера нельзя писать self.temperature = value, иначе он снова вызовет сам себя. Записывать следует в отдельный атрибут хранения.
Рабочий пример с инвариантом
Класс ниже запрещает температуру ниже абсолютного нуля и предоставляет вычисляемое представление в градусах Фаренгейта. Конструктор намеренно присваивает через публичное свойство: начальное значение проходит ту же проверку, что и последующие изменения.
class Temperature:
def __init__(self, celsius: float) -> None:
self.celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError('температура ниже абсолютного нуля')
self._celsius = float(value)
@property
def fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
sample = Temperature(20)
print(sample.celsius)
print(sample.fahrenheit)
sample.celsius = 25
print(sample.fahrenheit)Ожидаемый результат и границы решения
20.0
68.0
77.0fahrenheit доступен только для чтения: сеттер для него не объявлен. Property особенно уместен для дешёвого доступа, похожего по смыслу на поле. Долгую сетевую операцию или действие с заметным побочным эффектом лучше выразить обычным методом, чтобы стоимость вызова была видна читателю.
Когда сеттер вызывает сам себя
Характерная ошибка — бесконечная рекурсия в сеттере:
@celsius.setter
def celsius(self, value: float) -> None:
self.celsius = valueПрограмма завершится с RecursionError: maximum recursion depth exceeded. В traceback одно и то же присваивание будет повторяться много раз. Диагностика проста: найдите свойство, которое сеттер записывает под собственным публичным именем, и отделите интерфейс celsius от хранилища _celsius. Если вместо этого возникает AttributeError: property ... has no setter, проверьте наличие декоратора @celsius.setter и совпадение имён.
Практикум: свойство цены с налогом
Создайте класс Product со свойством price. Разрешайте только числа не меньше нуля, сохраняйте их как float, а доступное только для чтения свойство price_with_tax вычисляйте по ставке 20% как price * 1.2. Для цены 100 ожидается 120.0; затем проверьте изменение на 125.5 и отказ при -1. Самостоятельная проверка: все корректные присваивания должны проходить через один сеттер, отрицательная цена должна давать ValueError, а чтение price_with_tax — не менять состояние объекта.
Правила управляемых атрибутов
property нужен не ради скрытия каждого поля, а ради контроля значимого инварианта при сохранении атрибутного интерфейса. Геттер возвращает значение, сеттер проверяет запись, а внутреннее имя не должно повторно запускать свойство. Добавляйте property, когда появляется поведение; для простых открытых данных обычный атрибут остаётся нормальным решением.
Вопросы о property и сеттерах
Нужно ли создавать property для каждого атрибута?
Нет. Если чтение и запись ничего не проверяют и не вычисляют, прямой атрибут проще. Property можно добавить позже, не меняя выражения вида obj.name у вызывающего кода.
Можно ли сделать свойство только для чтения?
Да. Объявите геттер с @property и не добавляйте сеттер. Попытка присваивания тогда завершится AttributeError.
Зачем присваивать через property в __init__?
Так начальное состояние проходит общую проверку. Если конструктор напрямую пишет в _celsius, он может случайно создать объект, который сеттер никогда бы не разрешил.