Декоратор @property в Python: чистые, обратно совместимые геттеры и сеттеры
Освоение декоратора @property в Python
Встроенный декоратор @property в Python — мощный инструмент для инкапсуляции доступа к атрибутам. Он позволяет определять пользовательскую логику, выполняемую при чтении или записи атрибута, не заставляя вызывающих код менять свой код. В следующих разделах мы пройдёмся по реальному примеру — обработке преобразования температур — покажем, как применять ограничения, и объясним, почему @property является идиоматическим решением.
Класс без геттеров и сеттеров
Предположим, нам нужен простой класс, хранящий температуру в Цельсиях и умеющий конвертировать в Фаренгейты:
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
Использование простое:
# Создание нового объекта
human = Celsius()
# Установка температуры
human.temperature = 37
# Чтение температуры
print(human.temperature)
# Конвертация в Фаренгейты
print(human.to_fahrenheit())
Вывод
37 98.60000000000001
Результат с плавающей точкой ожидаем из-за двоичного представления десятичных чисел. При инспекции human.__dict__ вы увидите, что Python хранит значение в обычной записи словаря:
>>> human.__dict__
{'temperature': 37}
Добавление ограничений с геттерами и сеттерами
Реальные объекты часто нуждаются в валидации. Например, температура не может опускаться ниже абсолютного нуля (-273.15 °C). Один прямой способ — скрыть атрибут и предоставить явные методы-геттер/сеттер:
class Celsius:
def __init__(self, temperature=0):
self.set_temperature(temperature)
def to_fahrenheit(self):
return (self.get_temperature() * 1.8) + 32
# геттер
def get_temperature(self):
return self._temperature
# сеттер
def set_temperature(self, value):
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible.")
self._temperature = value
Теперь класс принудительно соблюдает нижнюю границу, но любой существующий код, обращавшийся к obj.temperature, должен быть обновлён для вызова obj.get_temperature() и obj.set_temperature(val). Это крупный рефакторинг для больших кодовых баз.
Кроме того, «приватные» переменные Python — лишь соглашение об именах — _temperature всё ещё доступен напрямую, нарушая инкапсуляцию. Вот где сияет @property.
Использование класса property
Python предлагает класс property, связывающий функции-геттер и сеттер с именем атрибута:
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
# геттер
def get_temperature(self):
print("Getting value...")
return self._temperature
# сеттер
def set_temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible")
self._temperature = value
# экспозиция как свойство
temperature = property(get_temperature, set_temperature)
С этой настройкой следующий код ведёт себя точно так же, как исходный простой класс, но с валидацией за кулисами:
human = Celsius(37)
print(human.temperature) # триггерит геттер
print(human.to_fahrenheit()) # использует геттер внутри
human.temperature = -300 # триггерит сеттер и вызывает ValueError
Вывод
Setting value... Getting value... 37 Getting value... 98.60000000000001 Setting value... Traceback (most recent call last): ... ValueError: Temperature below -273 is not possible
Обратите внимание: код, обращающийся к temperature, остался неизменным; только внутренняя реализация получила валидацию.
Совет: Фактические данные живут в _temperature; temperature — просто удобный, безопасный интерфейс.
Декоратор @property
Хотя класс property работает, синтаксис декораторов Python предлагает чистый, более читаемый способ достичь того же результата. Декоратор автоматически создаёт объект property и привязывает геттер и сеттер к имени атрибута.
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
@property
def temperature(self):
print("Getting value...")
return self._temperature
@temperature.setter
def temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273 is not possible")
self._temperature = value
Использование остаётся прежним:
human = Celsius(37)
print(human.temperature)
print(human.to_fahrenheit())
# Попытка установить невалидную температуру
try:
Celsius(-300)
except ValueError as e:
print(e)
Вывод
Setting value... Getting value... 37 Getting value... 98.60000000000001 Setting value... Traceback (most recent call last): ... ValueError: Temperature below -273 is not possible
Использование @property сохраняет пространство имён чистым, устраняет необходимость в отдельных именах геттеров/сеттеров и является рекомендуемым паттерном для большинства Python-разработчиков.
Python
- Освоение типов данных в Python: практическое руководство
- Мастерство операторов Python: Полное руководство
- Оператор pass в Python: практическое руководство
- Аргументы функций в Python: позиционные, именованные и параметры по умолчанию
- Словари в Python: создание, манипуляции и продвинутые техники
- Наследование в Python: концепции, синтаксис и практические примеры
- Перегрузка операторов в Python: практическое руководство
- Итераторы в Python: как они работают и как создать свои
- Замыкания в Python: как вложенные функции захватывают переменные
- Декоратор @property в Python: чистые, обратно совместимые геттеры и сеттеры