Что такое протокол дескрипторов в Python

Ответ

Протокол дескрипторов в Python — это механизм, позволяющий объекту (дескриптору) управлять доступом к атрибутам другого объекта (владельца). Дескриптор должен реализовать один или несколько из следующих методов:

  • __get__(self, instance, owner): для получения значения атрибута.
  • __set__(self, instance, value): для установки значения атрибута.
  • __delete__(self, instance): для удаления атрибута.

Дескрипторы работают только тогда, когда они являются атрибутами класса, а не экземпляра.

Пример: дескриптор для валидации положительных чисел

class PositiveNumber:
    def __set_name__(self, owner, name):
        # Сохраняем имя атрибута, к которому привязан дескриптор
        self.private_name = '_' + name

    def __get__(self, instance, owner):
        return getattr(instance, self.private_name)

    def __set__(self, instance, value):
        if not isinstance(value, (int, float)) or value <= 0:
            raise ValueError(f"Значение должно быть положительным числом, а не {value}")
        setattr(instance, self.private_name, value)

class Product:
    price = PositiveNumber()
    quantity = PositiveNumber()

    def __init__(self, price, quantity):
        self.price = price
        self.quantity = quantity

# Использование
book = Product(150, 10)
print(book.price)  # 150

try:
    book.price = -50  # Вызовет ValueError
except ValueError as e:
    print(e)

Типы дескрипторов:

  • Data Descriptor: реализует __set__ или __delete__. Имеет приоритет над словарем экземпляра (__dict__).
  • Non-Data Descriptor: реализует только __get__. Имеет меньший приоритет.

Этот протокол является основой для многих возможностей Python, включая @property, @staticmethod, @classmethod и работу слотов (__slots__).

Ответ 18+ 🔞

А, ну это же та самая магия, из- которой новички мозги на стенку размазывают! Протокол дескрипторов в Python — это, по сути, такой легальный способ подсунуть свой код в момент, когда кто-то пытается достучаться до атрибута объекта. Как будто ты ставишь своего человека на проходной, и он всех проверяет.

Чувак, это работает так: есть объект-дескриптор. Он не просто какая-то переменная, а хитрая жопа с методами-ловушками. Если его объявить как атрибут класса, то когда ты у какого-нибудь экземпляра этого класса попытаешься взять или записать значение в этот атрибут — сработает не простое присваивание, а твои методы.

Какие методы? Да вот эти, блядь:

  • __get__(self, instance, owner): Вызывается, когда кто-то пытается прочитать значение. instance — это тот самый экземпляр, у которого спрашивают, а owner — его родной класс.
  • __set__(self, instance, value): Вызывается, когда кто-то пытается записать значение. value — это то, что ему суют в руки.
  • __delete__(self, instance): Вызывается, когда кто-то пытается удалить атрибут командой del.

И главное, ёпта, правило номер один: эта магия работает только если дескриптор — атрибут класса. Если ты его в __init__ экземпляру прилепишь — будет обычное поле, и все твои __get__ и __set__ просто проигнорируют, в рот меня чих-пых!

Вот тебе живой пример: дескриптор, который орёт, если ему подсовывают не положительное число.

class PositiveNumber:
    def __set_name__(self, owner, name):
        # Тут мы хитрим: сохраняем имя, но с подчёркиванием, чтобы спрятать реальное значение
        self.private_name = '_' + name

    def __get__(self, instance, owner):
        # Когда просят значение — отдаём то, что спрятали
        return getattr(instance, self.private_name)

    def __set__(self, instance, value):
        # А вот тут проверяем! Не число или меньше нуля? Нахуй такое!
        if not isinstance(value, (int, float)) or value <= 0:
            raise ValueError(f"Значение должно быть положительным числом, а не {value}")
        # Если всё ок — прячем значение под заготовленное имя
        setattr(instance, self.private_name, value)

class Product:
    # Вот оно! Дескрипторы объявлены на уровне класса!
    price = PositiveNumber()
    quantity = PositiveNumber()

    def __init__(self, price, quantity):
        self.price = price  # Это вызовет __set__ у дескриптора price!
        self.quantity = quantity

# Пробуем
book = Product(150, 10)
print(book.price)  # 150 — сработал __get__

try:
    book.price = -50  # А это вызовет __set__, и он нас пошлёт
except ValueError as e:
    print(e)  # "Значение должно быть положительным числом, а не -50"

Ещё есть разделение на два типа, от которого приоритет зависит:

  • Data Descriptor: У него есть __set__ или __delete__. Эта тварь главнее словаря экземпляра (__dict__). Сначала Python спросит у неё.
  • Non-Data Descriptor: У него только __get__. Он скромнее, и если в самом экземпляре уже есть такое поле — возьмут его, а не побегут к дескриптору.

Этот протокол, блядь, — фундамент для кучи крутых штук в Python. Все эти @property, @staticmethod, @classmethod и даже __slots__ — они все внутри используют дескрипторы, просто прикидываются синтаксическим сахаром!