Какую роль выполняет метод __set__ в протоколе дескрипторов Python

Ответ

Метод __set__(self, instance, value) является частью протокола data descriptor (дескриптора данных) в Python. Он перехватывает операцию присваивания значения атрибуту, позволяя выполнить валидацию, преобразование типов или другую логику перед сохранением.

Он вызывается автоматически, когда происходит попытка присвоить значение атрибуту, управляемому дескриптором.

Параметры:

  • self: экземпляр самого дескриптора.
  • instance: экземпляр класса, в котором находится атрибут (например, MyClass()).
  • value: присваиваемое значение.

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

Чтобы дескриптор знал имя атрибута, с которым он работает, используется метод __set_name__ (Python 3.6+).

class NonNegative:
    def __set_name__(self, owner, name):
        # Сохраняем имя атрибута, например, 'price' или 'quantity'
        self.private_name = '_' + name

    def __get__(self, instance, owner):
        # Возвращаем значение из приватного атрибута экземпляра
        return getattr(instance, self.private_name)

    def __set__(self, instance, value):
        # Логика валидации перед присваиванием
        if value < 0:
            raise ValueError(f"Значение не может быть отрицательным.")
        # Сохраняем значение в приватный атрибут экземпляра
        setattr(instance, self.private_name, value)

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

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

# Использование
item = Product(100, 5)
print(f"Цена: {item.price}, Количество: {item.quantity}") # Цена: 100, Количество: 5

item.price = 200 # OK

# Эта строка вызовет ValueError
try:
    item.quantity = -1
except ValueError as e:
    print(e)

Ключевые моменты:

  1. Data Descriptor: Наличие метода __set__ (или __delete__) делает дескриптор дескриптором данных, который имеет более высокий приоритет, чем атрибуты в словаре экземпляра (__dict__).
  2. Валидация: Это основной сценарий использования __set__ — проверка данных на соответствие определенным правилам.
  3. Инкапсуляция: Позволяет скрыть внутреннюю логику хранения данных за простым интерфейсом атрибута.

Ответ 18+ 🔞

А, ну это же про тот самый __set__, который как злобный охранник на входе в клуб! Смотри, чувак, чтобы ты не пытался впихнуть в атрибут какую-нибудь дичь, этот метод всё проверит и либо пропустит, либо нахуй пошлёт с ошибкой.

Вот представь: у тебя есть класс Product с атрибутами price и quantity. И ты такой: "О, сейчас я сделаю item.price = -100, пусть магазин платит покупателям!" А __set__ тебе в ответ: "Нихуя, дружок-пирожок, отрицательная цена — это пиздец какой-то. Иди нахуй со своим Value Error'ом".

Вот как это выглядит под капотом, когда ты пытаешься что-то присвоить:

# Ты пишешь это:
item.price = 200

# А Python делает примерно это за кулисами:
Product.__dict__['price'].__set__(item, 200)

Вот тебе живой пример, чтобы мозг не взорвался. Делаем дескриптор для проверки, что число не отрицательное. Главная фишка — метод __set_name__, который запоминает, как его назвали в классе, чтобы потом знать, в какой приватный атрибут экземпляра всё складывать.

class NonNegative:
    def __set_name__(self, owner, name):
        # Запоминаем имя, но с подвохом. 'price' станет '_price' в экземпляре.
        # Хитрая жопа, да? Прячем данные, чтобы народ не шарился.
        self.private_name = '_' + name

    def __get__(self, instance, owner):
        # Когда кто-то спрашивает значение, мы достаём его из потайного кармана.
        return getattr(instance, self.private_name)

    def __set__(self, instance, value):
        # А вот и наш охранник на входе! Основная мясорубка.
        if value < 0:
            # Поймал на хуйне! Вызываем скандал.
            raise ValueError(f"Значение не может быть отрицательным.")
        # Всё чисто, проходи, клади свои данные в сейф.
        setattr(instance, self.private_name, value)

class Product:
    # Вот они, наши контролёры. Прикидываются обычными атрибутами класса.
    price = NonNegative()
    quantity = NonNegative()

    def __init__(self, price, quantity):
        # При создании объекта присваивание тоже идёт через __set__!
        # Так что валидация работает сразу, с первого дня.
        self.price = price
        self.quantity = quantity

# Поехали тестить
item = Product(100, 5)
print(f"Цена: {item.price}, Количество: {item.quantity}") # Всё ок, печатаем

item.price = 200 # Нормас, проходи

# А вот сейчас будет шоу
try:
    item.quantity = -1 # Пытаемся впихнуть минус один товар. Гениально!
except ValueError as e:
    print(e) # И получаем по ебалу от дескриптора: "Значение не может быть отрицательным."

Запомни три вещи, чтобы не выглядеть как манда с ушами:

  1. Data Descriptor — это сила. Если у дескриптора есть __set__ или __delete__, он становится главнее любого атрибута в __dict__ экземпляра. Он перехватывает ВСЕ попытки что-то записать. Это овердохуища власти.
  2. Валидация — его родная стихия. Его основная работа — стоять на страже и не пускать всякую хуйню типа отрицательных цен, пустых строк или ебучих None туда, где они не нужны.
  3. Инкапсуляция на максимум. Снаружи класс Product выглядит просто и чисто: просто price и quantity. А вся грязная работа по проверке и хранению в _price и _quantity спрятана внутри дескриптора. Красота, ёпта!

В общем, __set__ — это твой личный телохранитель для атрибутов класса. Хочешь, чтобы данные всегда были в порядке — заведи такого.