← Назад к вопросам

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

2.0 Middle🔥 181 комментариев
#DevOps и инфраструктура#Django

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Протокол дескрипторов (Descriptor Protocol)

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

Магические методы дескриптора

Протокол дескрипторов определяется тремя магическими методами:

class Descriptor:
    def __get__(self, obj, objtype=None):
        """Вызывается при получении атрибута (attr = instance.attr)"""
        return "получение"
    
    def __set__(self, obj, value):
        """Вызывается при установке атрибута (instance.attr = value)"""
        print(f"установка значения {value}")
    
    def __delete__(self, obj):
        """Вызывается при удалении атрибута (del instance.attr)"""
        print("удаление атрибута")

Data descriptor содержит __get__ и __set__ (или __delete__)
Non-data descriptor содержит только __get__

Простой пример

class Temperature:
    """Дескриптор для хранения температуры в градусах Цельсия"""
    
    def __init__(self, name: str):
        self.name = name
        self.value = 0
    
    def __get__(self, obj, objtype=None):
        """Возвращаем температуру в Цельсии"""
        if obj is None:
            return self
        print(f"Получаем значение {self.name}")
        return self.value
    
    def __set__(self, obj, value: float):
        """Проверяем значение перед установкой"""
        if value < -273.15:
            raise ValueError("Температура не может быть ниже абсолютного нуля")
        print(f"Устанавливаем {self.name} = {value}°C")
        self.value = value
    
    def __delete__(self, obj):
        print(f"Удаляем {self.name}")
        self.value = None

class Sensor:
    """Класс, использующий дескриптор"""
    temperature = Temperature("temperature")
    
    def __init__(self):
        self.temperature = 20.0

# Использование
sensor = Sensor()
print(sensor.temperature)  # "Получаем значение temperature" → 20.0
sensor.temperature = 25.0  # "Устанавливаем temperature = 25.0°C"
del sensor.temperature  # "Удаляем temperature"

Практический пример: валидированное свойство

class ValidatedString:
    """Дескриптор для строк с минимальной длиной"""
    
    def __init__(self, min_length: int = 1, max_length: int = 255):
        self.min_length = min_length
        self.max_length = max_length
        self.name = None
    
    def __set_name__(self, owner, name):
        """Автоматически вызывается при присваивании дескриптору имена атрибута"""
        self.name = name
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(f"_{self.name}", "")
    
    def __set__(self, obj, value: str):
        if not isinstance(value, str):
            raise TypeError(f"{self.name} должно быть строкой")
        if len(value) < self.min_length:
            raise ValueError(
                f"{self.name} должно быть не менее {self.min_length} символов"
            )
        if len(value) > self.max_length:
            raise ValueError(
                f"{self.name} должно быть не более {self.max_length} символов"
            )
        obj.__dict__[f"_{self.name}"] = value

class User:
    username = ValidatedString(min_length=3, max_length=20)
    email = ValidatedString(min_length=5, max_length=100)
    
    def __init__(self, username: str, email: str):
        self.username = username
        self.email = email

# Использование
try:
    user = User("ab", "test@example.com")  # Ошибка: username слишком короткий
except ValueError as e:
    print(f"Ошибка: {e}")

user = User("alice", "alice@example.com")
print(user.username)  # "alice"

Пример: вычисляемое свойство (computed property)

class Radius:
    """Дескриптор для радиуса круга"""
    
    def __init__(self):
        self.value = 0
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self.value
    
    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError("Радиус должен быть положительным")
        self.value = value

class Circle:
    radius = Radius()
    
    @property
    def area(self):
        """Площадь круга (вычисляется динамически)"""
        import math
        return math.pi * self.radius ** 2
    
    @property
    def circumference(self):
        """Периметр круга"""
        import math
        return 2 * math.pi * self.radius

circle = Circle()
circle.radius = 5
print(f"Площадь: {circle.area:.2f}")  # 78.54
print(f"Периметр: {circle.circumference:.2f}")  # 31.42

Дескрипторы в Python: свойства (properties)

class Temperature:
    def __init__(self, celsius: float = 0):
        self._celsius = celsius
    
    @property
    def celsius(self) -> float:
        """Получить температуру в Цельсии"""
        return self._celsius
    
    @celsius.setter
    def celsius(self, value: float):
        """Установить температуру в Цельсии"""
        if value < -273.15:
            raise ValueError("Температура ниже абсолютного нуля")
        self._celsius = value
    
    @property
    def fahrenheit(self) -> float:
        """Получить температуру в Фаренгейте"""
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value: float):
        """Установить температуру в Фаренгейте"""
        self._celsius = (value - 32) * 5/9

# Использование
temp = Temperature()
temp.celsius = 0
print(f"Celsius: {temp.celsius}, Fahrenheit: {temp.fahrenheit}")  # 0, 32.0
temp.fahrenheit = 68  # Устанавливаем через Фаренгейты
print(f"Celsius: {temp.celsius}")  # 20.0

Дескрипторы для методов

class BoundMethod:
    """Дескриптор для связывания методов"""
    
    def __init__(self, func):
        self.func = func
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self.func
        # Возвращаем функцию, привязанную к объекту
        def bound_method(*args, **kwargs):
            return self.func(obj, *args, **kwargs)
        return bound_method

class MyClass:
    def __init__(self, value: int):
        self.value = value
    
    def method(self):
        return f"Value: {self.value}"

obj = MyClass(42)
print(obj.method())  # "Value: 42"

Порядок разрешения атрибутов (MRO для дескрипторов)

  1. Data descriptor__get__ и __set__ или __delete__) — высший приоритет
  2. Instance dict (obj.__dict__["attr"])
  3. Non-data descriptor (только с __get__)
  4. Class dict (type(obj).__dict__["attr"])
  5. MRO (иерархия наследования)
class DataDescriptor:
    def __get__(self, obj, objtype=None):
        return "data descriptor"
    def __set__(self, obj, value):
        pass

class NonDataDescriptor:
    def __get__(self, obj, objtype=None):
        return "non-data descriptor"

class MyClass:
    data_desc = DataDescriptor()
    non_data_desc = NonDataDescriptor()

obj = MyClass()
obj.__dict__["data_desc"] = "instance value"
obj.__dict__["non_data_desc"] = "instance value"

print(obj.data_desc)  # "data descriptor" (data descriptor имеет приоритет)
print(obj.non_data_desc)  # "instance value" (instance dict имеет приоритет)

Практическое применение

  • Валидация данных — проверка типов и значений
  • Lazy loading — отложенная загрузка данных
  • Кэширование — сохранение вычисленных значений
  • Вычисляемые свойства — динамическое вычисление значений
  • ORM (SQLAlchemy, Django ORM) — управление атрибутами моделей
  • Декораторы — функции работают через дескрипторы

Дескрипторы — это мощный инструмент для контроля доступа к атрибутам и создания сложной логики в Python-классах.