Python. Свойства

Перевод статьи Python @propertyopen in new window.

Python поддерживает концепцию свойств упрощающих объектно ориентированное программирование. Прежде чем погрузится глубже в детали свойств, рассмотрим зачем могут быть нужны свойства.

Начальный пример

Представим что вы решили сделать класс хранящий температуру в градусах Цельсия. Он также должен реализовывать метод для конвертации температуры в градусы Фаренгейта. Реализуем класс так:

class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32
1
2
3
4
5
6

Затем создаём объект этого класса и меняем значение температуры как пожелаем:

>>> # создание нового объекта
>>> man = Celsius()
>>> # установка температуры
>>> man.temperature = 37
>>> # получение значения температуры
>>> man.temperature
37
>>> # конвертация в градусы Фаренгейта
>>> man.to_fahrenheit()
98.60000000000001
1
2
3
4
5
6
7
8
9
10

Дополнительные десятичные разряды при конвертации в градусы Фаренгейта происходят из-за арифметической ошибки с плавающей запятой (попробуйте сложить 1.1 + 2.2 в интерпретаторе).

Когда мы присваиваем или извлекаем атрибут объекта, такой как temperature, Python ищет его в словаре объекта __dict__.

>>> man.__dict__
{'temperature': 37}
1
2

Внутри интерпретатора man.temperature становится man.__dict__['temperature'].

Теперь представим что наш класс стал популярным, много клиентов стали использовать его в своих программах. Однажды, важный клиент пришёл в нам и сообщил что температура не может быть ниже -273 градусов Цельсия. Затем он попросил реализовать это ограничение значения. Прислушавшись к этому предложению мы реализуем его и выпускаем новую версию нашего класса.

Использование геттеров и сеттеров

Очевидным решением будет скрыть атрибут temperature (сделать его приватным) и определить интерфейс в виде геттера и сеттера для управления полем.

class Celsius:
    def __init__(self, temperature = 0):
        self.set_temperature(temperature)

    def to_fahrenheit(self):
        return (self.get_temperature() * 1.8) + 32

    # new update
    def get_temperature(self):
        return self._temperature

    def set_temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        self._temperature = value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Добавлены методы get_temperature() и set_temperature(), имя поле temperature заменено на _temperature. Подчёркивание в начале имени используется для обозначения приватных переменных в Python.

>>> c = Celsius(-277)
Traceback (most recent call last):
...
ValueError: Temperature below -273 is not possible
>>> c = Celsius(37)
>>> c.get_temperature()
37
>>> c.set_temperature(10)
>>> c.set_temperature(-300)
Traceback (most recent call last):
...
ValueError: Temperature below -273 is not possible
1
2
3
4
5
6
7
8
9
10
11
12

Изменения успешно реализуют новые ограничения. Мы больше не можем установить температуру ниже -273.

В языке Python нет приватных переменных. Существуют нормы которым придерживаются разработчики, но язык сам не применяет ограничения.

>>> c._temperature = -300
>>> c.get_temperature()
-300
1
2
3

Но это не является большой проблемой. Гораздо хуже то что все клиенты использующие предыдущую версию класса должны изменить свой код с obj.temperature на obj.get_temperature() и все присвоения obj.temperature = val на obj.set_temperature(val). Наше обновление не поддерживает обратную совместимость. Свойства помогают решить эту проблему.

Свойства

Добавление ограничений в класс в стиле Python:

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):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value")
        self._temperature = value

    temperature = property(get_temperature,set_temperature)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Мы добавили функции print() внутрь get_temperature() и set_temperature() чтобы ясно видеть когда они запускаются.

Последняя строка кода создаёт объект свойства temperature. Свойство присоединяет некоторый код (get_temperature и set_temperature) к атрибутту класса (temperature).

Код извлекающий значение из temperature автоматически вызывает get_temperature() вместо поиска по словарю (__dict__). Таким же образом, код присваивающий значение temperature автоматически вызовет set_temperature().

>>> c = Celsius()
Setting value
1
2

После создания объекта появляется сообщение из метода set_temperature(). Это происходит потому что при создании объекта вызывается метод __init__(), а в нём приходит присвоение self.temperature = temperature. Присвоение автоматически вызывает set_temperature().

>>> c.temperature
Getting value
0
1
2
3

Любой запрос к полю, такой как c.temperature автоматически вызывает get_temperature(). Ещё несколько примеров:

>>> c.temperature = 37
Setting value
>>> c.to_fahrenheit()
Getting value
98.60000000000001
1
2
3
4
5

При использовании свойств мы изменяем класс и реализуем ограничения значения без изменений в клиентском коде. Наша реализация обладает обратной совместимостью.

Обратите внимание, что значение температуры хранится в приватной переменной _temperature. Атрибут temperature является объектом свойства который предоставляет интерфейс доступа к этой приватной переменной.

Глубокое погружение в свойства

В Python, property() это встроенная функция с сигнатурой

property(fget=None, fset=None, fdel=None, doc=None)
1
  • fget - функция для получения значения атрибута
  • fset - функция для установки значения атрибута
  • fdel - функция для удаления атрибута
  • doc - строка с комментарием

Все аргументы не обязательны, объект свойства может быть создан так:

>>> property()
<property object at 0x0000000003239B38>
1
2

Объект свойства имеет три метода: getter(), setter() и deleter() для указания fget, fset и fdel после создания объекта. Это означает что строка

temperature = property(get_temperature, set_temperature)
1

эквивалентна коду

# make empty property
temperature = property()
# assign fget
temperature = temperature.getter(get_temperature)
# assign fset
temperature = temperature.setter(set_temperature)
1
2
3
4
5
6

Используя декоратор мы можем избежать создания лишних имён get_temperature и set_temperature.

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):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value")
        self._temperature = value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Использования декоратора простой и рекомендуемый способ создания свойств.

Последниее изменение: 24.08.2023, 06:42:55