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

У самому методі ми перевіримо це, і змінимо логіку:

  1. якщо зчитується атрибут класу, то результатом у методі буде власний атрибут count

  2. якщо зчитується атрибут об’єкту, то результатом буде власний атрибут 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 просто | з Коропом

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Вітя
Вітя@kor0p

Python SDET @ ajax.systems

811Прочитань
10Автори
17Читачі
Підтримати
На Друкарні з 14 квітня

Більше від автора

Вам також сподобається

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Вам також сподобається