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
2
3
4
5
6
Затем создаём объект этого класса и меняем значение температуры как пожелаем:
>>> # создание нового объекта
>>> man = Celsius()
>>> # установка температуры
>>> man.temperature = 37
>>> # получение значения температуры
>>> man.temperature
37
>>> # конвертация в градусы Фаренгейта
>>> man.to_fahrenheit()
98.60000000000001
2
3
4
5
6
7
8
9
10
Дополнительные десятичные разряды при конвертации в градусы Фаренгейта происходят из-за арифметической ошибки с плавающей запятой (попробуйте сложить 1.1 + 2.2
в интерпретаторе).
Когда мы присваиваем или извлекаем атрибут объекта, такой как temperature
, Python ищет его в словаре объекта __dict__
.
>>> man.__dict__
{'temperature': 37}
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
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
2
3
4
5
6
7
8
9
10
11
12
Изменения успешно реализуют новые ограничения. Мы больше не можем установить температуру ниже -273.
В языке Python нет приватных переменных. Существуют нормы которым придерживаются разработчики, но язык сам не применяет ограничения.
>>> c._temperature = -300
>>> c.get_temperature()
-300
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)
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
2
После создания объекта появляется сообщение из метода set_temperature()
. Это происходит потому что при создании объекта вызывается метод __init__()
, а в нём приходит присвоение self.temperature = temperature
. Присвоение автоматически вызывает set_temperature()
.
>>> c.temperature
Getting value
0
2
3
Любой запрос к полю, такой как c.temperature
автоматически вызывает get_temperature()
. Ещё несколько примеров:
>>> c.temperature = 37
Setting value
>>> c.to_fahrenheit()
Getting value
98.60000000000001
2
3
4
5
При использовании свойств мы изменяем класс и реализуем ограничения значения без изменений в клиентском коде. Наша реализация обладает обратной совместимостью.
Обратите внимание, что значение температуры хранится в приватной переменной _temperature
. Атрибут temperature
является объектом свойства который предоставляет интерфейс доступа к этой приватной переменной.
Глубокое погружение в свойства
В Python, property()
это встроенная функция с сигнатурой
property(fget=None, fset=None, fdel=None, doc=None)
- fget - функция для получения значения атрибута
- fset - функция для установки значения атрибута
- fdel - функция для удаления атрибута
- doc - строка с комментарием
Все аргументы не обязательны, объект свойства может быть создан так:
>>> property()
<property object at 0x0000000003239B38>
2
Объект свойства имеет три метода: 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)
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Использования декоратора простой и рекомендуемый способ создания свойств.