Python надає вбудований декоратор @property, який значно спрощує використання геттерів та сеттерів у об’єктно-орієнтованому програмуванні. Перш ніж ми розглянемо, що є декоратором @property, давайте спочатку розберемося, навіщо він взагалі потрібен.
Клас без геттерів та сеттерів
Припустимо, що ми вирішили створити клас, який зберігає температуру в градусах за Цельсієм, та метод для конвертації температури із градусів за Цельсієм у градуси за Фаренгейтом. Це можна зробити наступним чином:
|
1 2 3 4 5 6 |
class Celsius: def __init__(self, temperature = 0): self.temperature = temperature def to_fahrenheit(self): return (self.temperature * 1.8) + 32 |
Ми можемо створювати об’єкти на основі цього класу та маніпулювати атрибутом temperature на свій розсуд:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# Базові методи встановлення та отримання атрибутів в Python 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
Примітка: Тут зайві десяткові знаки при перетворенні в градуси за Фаренгейтом викликані помилкою арифметики з числами типу з плаваючою крапкою.
Таким чином, кожного разу, коли ми присвоюємо або отримуємо будь-який атрибут об’єкта, наприклад, temperature, як показано вище, Python шукає його у вбудованому атрибуті словника __dict__ як:
|
1 2 |
print(human.__dict__) # Результат: {'temperature': 37} |
Тому human.temperature внутрішньо стає human.__dict__['temperature'].
Використання геттерів та сеттерів
Припустимо, що ми хочемо розширити можливості використання класу Celsius, визначеного вище. Ми знаємо, що температура будь-якого об’єкта не може бути нижчою від -273.15 градусів за Цельсієм. Оновимо наш код, щоб реалізувати це обмеження.
Одним із рішень є приховування атрибута temperature (зробимо його приватним) та визначення нових методів (геттера та сеттера) для роботи з ним.
Примітка: Геттер — це функція, яка повертає значення змінних членів (атрибутів) класу. Сеттер — це функція, яка дозволяє надавати значення змінним-членам (атрибутам) класу.
Це можна зробити наступним чином:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Створюємо методи геттера та сеттера 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 |
З’явилися два нові методи: get_temperature() та set_temperature().
Крім того, значення temperature було замінено на _temperature. Знак підкреслення _ на початку використовується для позначення приватних змінних у Python.
Тепер додамо цю реалізацію до нашої програми:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# Створюємо методи геттера та сеттера 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 # Створюємо новий об'єкт, викликається метод set_temperature() human = Celsius(37) # Отримуємо атрибут температури за допомогою геттера print(human.get_temperature()) # Виконуємо конвертацію температури в градуси за Фаренгейтом print(human.to_fahrenheit()) # Реалізація нових обмежень human.set_temperature(-300) # Виконуємо конвертацію температури в градуси за Фаренгейтом 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 немає. Просто існують норми, яких необхідно дотримуватися. Сам Python не накладає жодних обмежень.
Однак більш серйозна проблема, пов’язана з цим оновленням, полягає в тому, що всі програми, які використовують наш клас, повинні змінити свій код з obj.temperature на obj.get_temperature(), а всі вирази типу obj.temperature = val на obj.set_temperature(val).
Такий рефакторинг може викликати проблеми під час роботи з сотнями тисяч рядків коду. Загалом виходить, що наше оновлення не має зворотної сумісності. Саме тут на допомогу приходить декоратор @property.
Клас property
Пітонічний спосіб вирішення цієї проблеми полягає у використанні класу property. Наприклад:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# Використання класу 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 # Створюємо об'єкт класу property temperature = property(get_temperature, set_temperature) |
Ми додали функцію print() всередині get_temperature() і set_temperature(), щоб наочно бачити, що вони виконуються.
Останній рядок коду створює об’єкт temperature класу property. Простіше кажучи, property прикріплює певний код (get_temperature та set_temperature) до атрибуту-члена, до якого здійснюється доступ (temperature).
Повна програма:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# Використання класу 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 # Створюємо об'єкт класу property temperature = property(get_temperature, set_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() була викликана навіть при створенні об’єкта.
|
1 |
human = Celsius(37) # виведе "Setting value..." |
Чи можете ви здогадатися, чому?
Причина полягає в тому, що під час створення об’єкта викликається метод __init__(). Цей метод має рядок self.temperature = temperature. Цей вираз автоматично викликає set_temperature().
Аналогічно, будь-який виклик типу c.temperature автоматично викликає get_temperature(). Ось що робить клас property. Використовуючи property, жодних змін у реалізації обмеження не потрібно. Таким чином, наша реалізація є зворотно сумісною.
Примітка: Фактичне значення температури зберігається у приватній змінній _temperature. Атрибут temperature — це об’єкт класу property, який забезпечує інтерфейс цієї приватної змінної.
Декоратор @property
В Python функція property() є вбудованою функцією, яка створює і повертає об’єкт property. Синтаксис цієї функції наступний:
|
1 |
property(fget=None, fset=None, fdel=None, doc=None) |
Тут:
fget — це функція для отримання значення атрибута;
fset — це функція для встановлення значення атрибута;
fdel — це функція для видалення атрибута;
doc — це рядок (як коментар).
Як видно з реалізації, ці аргументи функції не є обов’язковими.
Об’єкт property має три методи: getter(), setter() і deleter(), що дозволяють вказати fget, fset та fdel. Це означає, що наступний рядок:
|
1 |
temperature = property(get_temperature,set_temperature) |
Може бути розбитий на:
|
1 2 3 4 5 6 7 8 |
# Створюємо порожній property temperature = property() # Присвоюємо fget temperature = temperature.getter(get_temperature) # Присвоюємо fset temperature = temperature.setter(set_temperature) |
Ці два фрагменти коду рівнозначні.
Вищенаведена конструкція може бути реалізована у вигляді декораторів. Ми можемо навіть не визначати ідентифікатори get_temperature та set_temperature, оскільки вони не потрібні.
Для цього ми повторно використовуємо ідентифікатор temperature при визначенні наших функцій геттера та сеттера. Наприклад, реалізація за допомогою декоратора @property:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# Використовуємо декоратор @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()) coldest_thing = Celsius(-300) |
Результат:
Setting value...
Getting value...
37
Getting value...
98.60000000000001
Setting value...
Traceback (most recent call last):
File "", line 29, in
File "", line 4, in __init__
File "", line 18, in temperature
ValueError: Temperature below -273 is not possible
Дана реалізація проста та ефективна. Це рекомендований спосіб використання декоратора property.
