Декоратор @property в Python
Python предоставляет встроенный декоратор @property, который значительно упрощает использование геттеров и сеттеров в Объектно-ориентированное программирование в Python.
Прежде чем углубляться в детали того, что такое декоратор @property, давайте сначала построим понимание того, зачем он вообще нужен.
Класс без геттеров и сеттеров
Предположим, мы решили создать класс, который хранит температуру в градусах Цельсия. И также он будет реализовывать метод для преобразования температуры в градусы Фаренгейта.
Один из способов сделать это:
class Celsius:
def __init__(self, temperature = 0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
Мы можем создавать объекты из этого класса и манипулировать атрибутом temperature по своему усмотрению:
# Basic method of setting and getting attributes in Python
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
# Create a new object
human = Celsius()
# Set the temperature
human.temperature = 37
# Get the temperature attribute
print(human.temperature)
# Get the to_fahrenheit method
print(human.to_fahrenheit())
Вывод
37
98.60000000000001
Здесь лишние десятичные знаки при преобразовании в Фаренгейты обусловлены ошибкой арифметики с плавающей точкой.
Итак, всякий раз, когда мы присваиваем или извлекаем какой-либо атрибут объекта, такой как temperature, как показано выше, Python ищет его во встроенном словарном атрибуте объекта __dict__:
print(human.__dict__)
# Output: {'temperature': 37}
Следовательно, human.temperature внутри становится human.__dict__['temperature'].
Использование геттеров и сеттеров
Предположим, мы хотим расширить применимость класса Celsius, определённого выше. Мы знаем, что температура любого объекта не может опуститься ниже -273.15 градусов Цельсия.
Давайте обновим наш код, чтобы реализовать это ограничение значения.
Очевидным решением для приведённого выше ограничения будет скрыть атрибут temperature (сделать его приватным) и определить новые методы геттера и сеттера для манипуляции им.
Это можно сделать следующим образом:
# Making Getters and Setter methods
class Celsius:
def __init__(self, temperature=0):
self.set_temperature(temperature)
def to_fahrenheit(self):
return (self.get_temperature() * 1.8) + 32
# getter method
def get_temperature(self):
return self._temperature
# setter method
def set_temperature(self, value):
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible.")
self._temperature = value
Как видно, приведённый выше метод вводит два новых метода get_temperature() и set_temperature().
Кроме того, temperature была заменена на _temperature. Подчёркивание _ в начале используется для обозначения приватных переменных в Python.
Теперь давайте используем эту реализацию:
# Create a new object, set_temperature() internally called by __init__
human = Celsius(37)
# Get the temperature attribute via a getter
print(human.get_temperature())
# Get the to_fahrenheit method, get_temperature() called by the method itself
print(human.to_fahrenheit())
# new constraint implementation
human.set_temperature(-300)
# Get the to_fahreheit method
print(human.to_fahrenheit())
Вывод
37
98.60000000000001
Traceback (most recent call last):
File "<string>", line 30, in <module>
File "<string>", line 16, in set_temperature
ValueError: Temperature below -273.15 is not possible.
Это обновление успешно реализовало новое ограничение. Нам больше не разрешается устанавливать температуру ниже -273.15 градусов Цельсия.
Примечание
Приватные переменные на самом деле не существуют в Python. Существуют просто нормы, которым нужно следовать. Сам язык не применяет никаких ограничений.
Однако более серьёзная проблема с приведённым выше обновлением заключается в том, что все программы, которые реализовали наш предыдущий класс, должны изменить свой код с obj.temperature на obj.get_temperature(), а все выражения, такие как obj.temperature = val на obj.set_temperature(val).
Этот рефакторинг может вызвать проблемы при работе с сотнями тысяч строк кода.
В целом, наше новое обновление не было обратно совместимым. Здесь на помощь приходит @property.
Класс property
Pythonic-способ решения вышеуказанной проблемы — использовать класс property. Вот как мы можем обновить наш код:
# using property class
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
# getter
def get_temperature(self):
print("Getting value...")
return self._temperature
# setter
def set_temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible")
self._temperature = value
# creating a property object
temperature = property(get_temperature, set_temperature)
Мы добавили функцию print() внутри get_temperature() и set_temperature(), чтобы чётко наблюдать, что они выполняются.
Последняя строка кода создаёт объект property temperature. Проще говоря, property прикрепляет некоторый код (get_temperature и set_temperature) к доступам к атрибуту члена (temperature).
Давайте используем этот обновлённый код:
human = Celsius(37)
print(human.temperature)
print(human.to_fahrenheit())
human.temperature = -300
Вывод
Setting value...
Getting value...
37
Getting value...
98.60000000000001
Setting value...
Traceback (most recent call last):
File "<string>", line 31, in <module>
File "<string>", line 18, in set_temperature
ValueError: Temperature below -273 is not possible
Как видно, любой код, который извлекает значение temperature, будет автоматически вызывать get_temperature() вместо поиска в словаре (__dict__).
Аналогично, любой код, который присваивает значение temperature, будет автоматически вызывать set_temperature().
Мы даже можем видеть выше, что set_temperature() был вызван даже тогда, когда мы создавали объект:
human = Celsius(37) # prints Setting value...
Можете ли вы догадаться, почему?
Причина в том, что когда объект создаётся, вызывается метод __init__(). Этот метод содержит строку self.temperature = temperature. Это выражение автоматически вызывает set_temperature().
Аналогично, любой доступ, такой как c.temperature, автоматически вызывает get_temperature(). Вот что делает property.
Используя property, мы видим, что никаких изменений не требуется в реализации ограничения значения. Таким образом, наша реализация обратно совместима.
Примечание
Фактическое значение температуры хранится в приватной переменной _temperature. Атрибут temperature — это объект property, который обеспечивает интерфейс к этой приватной переменной.
Декоратор @property
В Python property() — это встроенная функция, которая создаёт и возвращает объект property. Синтаксис этой функции:
property(fget=None, fset=None, fdel=None, doc=None)
Здесь:
fget— функция для получения значения атрибутаfset— функция для установки значения атрибутаfdel— функция для удаления атрибутаdoc— строка (как комментарий)
Как видно из реализации, эти аргументы функции являются необязательными.
Объект property имеет три метода: getter(), setter() и deleter() для определения fget, fset и fdel позднее. Это означает, что строка:
temperature = property(get_temperature,set_temperature)
может быть разбита на:
# make empty property
temperature = property()
# assign fget
temperature = temperature.getter(get_temperature)
# assign fset
temperature = temperature.setter(set_temperature)
Эти два куска кода эквивалентны.
Программисты, знакомые с декораторами Python, могут распознать, что приведённую выше конструкцию можно реализовать как декораторы.
Мы даже можем не определять имена get_temperature и set_temperature, так как они не нужны и загрязняют пространство имён класса.
Для этого мы повторно используем имя temperature при определении наших функций геттера и сеттера. Давайте посмотрим, как реализовать это в виде декоратора:
class Celsius:
def __init__(self, temperature=0):
# when creating the object, the setter method is called automatically
self.temperature = temperature
def to_fahrenheit(self):
# convert the temperature to Fahrenheit
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...")
# ensure the temperature does not go below absolute zero
if value < -273.15:
raise ValueError("Temperature below -273.15°C is not possible")
self._temperature = value
# create an object with a valid temperature
human = Celsius(37)
# print the temperature in Celsius
print(human.temperature)
# print the temperature in Fahrenheit
print(human.to_fahrenheit())
# attempting to create an object with a temperature below -273.15°C will raise an exception
try:
coldest_thing = Celsius(-300)
except ValueError as e:
print(e)
Вывод
Setting value...
Getting value...
37
Getting value...
98.60000000000001
Setting value...
ValueError: Temperature below -273 is not possible
Приведённая выше реализация проста и эффективна. Это рекомендуемый способ использования property.