Декоратор @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.