Можливо, навіть далеко не всі чули, що таке дескриптори, але точно всі використовували їх
Я кажу це так впевнено, оскільки @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 просто | з Коропом