Можливо, навіть далеко не всі чули, що таке дескриптори, але точно всі використовували їх
Я кажу це так впевнено, оскільки @property — є дескриптором 😮
Вступ
Оригінально, цей клас не задумувався як декоратор, а лише як явний транслятор між атрибутом та набором методів — геттер, сеттер та делеттер
Виглядати це має ось так:
# https://docs.python.org/uk/3/library/functions.html#property
class C:
def __init__(self):
self._x = None
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
І “під капотом” у @property відбувається вся магія
Як зробити власний аналог — описано тут. Я ж хочу спробувати пояснити дескриптори простішим чином 😉
class CountDescriptor:
def __init__(self, base_count=0):
self.count = base_count
def __get__(self, instance, owner):
if instance is None: # зчитуємо атрибут класу
return self.count
return self.count + instance.count
class Container:
total = CountDescriptor(10)
def __init__(self, count=15):
self.count = count
print(Container.total)
print(Container().total)
print(Container(50).total)
Виглядає складно? 😨
Нічьо, зараз розберемось 😎
Клас CountDescriptor
має метод __get__
, отже є дескриптором
У цього класу також є атрибут count
, що задається у конструкторі
Якщо атрибут (у нас — total
) є дескриптором, то при спробі зчитати атрибут, Пайтон виконає у нього метод __get__
, передавши першим аргументом об’єкт, а другим — клас цього об’єкту
Якщо ж зчитується атрибут класу, як перший атрибут буде передано None
У самому методі ми перевіримо це, і змінимо логіку:
якщо зчитується атрибут класу, то результатом у методі буде власний атрибут
count
якщо зчитується атрибут об’єкту, то результатом буде власний атрибут
count
і доданий до нього атрибутcount
переданого об’єкту
Тепер перейдемо до класу Container
Цей клас має звичайний атрибут count
, зі значенням за замовчуванням 10, у конструкторі
І головне, що має цей клас — атрибут-дескриптор: total
Отже:
для атрибуту
total
класуContainer
виконається умова 1,
відповідно значення буде 10для атрибуту
total
об’єктуContainer()
виконається умова 2,
відповідно значення буде 25 (10 + 15)для атрибуту
total
об’єктуContainer(50)
виконається умова 2,
відповідно значення буде 60 (10 + 50)
Якщо все ж складно, можете погратись з цим кодом у моєму JupyterLite, або продебагати код і побачити як усі дії відбуваються наяву 🤔
Продебагати онлайн можна у Python Tutor
Усе те саме працює і для зміни значення атрибуту (__set__
) та видалення атрибуту (__delete__
). Якщо хоча б один з цих методів є у класі, його можна використовувати як дескриптор 🤓
Для чого ж можна використовувати дескриптори у реальному світі?)
Для всього 👀
Тобто, це всього лиш інструмент 😅
Він допомагає приховати складну реалізацію, при цьому залишивши дуже зручний інструмент у вигляді атрибуту
З поширених прикладів, це поля БД у ORM (зміна атрибута у об’єкті потім повпливає на зміни у БД), вищезгадані @property, а також його друзі @classmethod та @staticmethod, і навіть самі методи як такі 🤯
Це все прямим текстом пише в документації
За допомогою дескрипторів можна просто реалізувати кешування атрибутів, які можуть рідко використовуватись
А ще, якби це дивно не виглядало, методи дескриптора можуть бути асинхронними, і це відкриває ще більше можливостей, звісно зі своєю ложкою дьогтю у вигляді заплутаного використання 🥲
Післямови не буде, бо я такого не вмію 😢
Якщо вам подобається такий контент, підписуйтесь на мій телеграм-канал:
Python просто | з Коропом