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

Как создать свой дескриптор?

3.0 Senior🔥 61 комментариев
#Python Core#Архитектура и паттерны

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

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

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

Как создать свой дескриптор

Дескриптор — это объект, который имеет методы __get__, __set__ и/или __delete__. Это позволяет контролировать доступ к атрибутам класса и создавать свойства с настраиваемым поведением.

1. Базовый дескриптор

class Descriptor:
    """Базовый дескриптор"""
    def __get__(self, obj, objtype=None):
        print("Getting value")
        return getattr(obj, '_value', None)
    
    def __set__(self, obj, value):
        print(f"Setting value to {value}")
        obj._value = value
    
    def __delete__(self, obj):
        print("Deleting value")
        del obj._value

class MyClass:
    x = Descriptor()

obj = MyClass()
obj.x = 10      # Вызывает __set__
print(obj.x)    # Вызывает __get__
del obj.x       # Вызывает __delete__

2. Non-data дескриптор (только get)

class Lazy:
    """Lazy-loaded свойство"""
    def __init__(self, func):
        self.func = func
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        print(f"Computing {self.func.__name__}...")
        return self.func(obj)

class Person:
    def __init__(self, name):
        self.name = name
    
    @Lazy
    def full_info(self):
        # Вычисляется только при доступе
        return f"Person: {self.name}"

p = Person("Alice")
print(p.full_info)  # Вычисляется один раз
print(p.full_info)  # Ещё раз вычисляется (no caching)

3. Валидирующий дескриптор (data дескриптор)

class ValidatedString:
    """Дескриптор, валидирующий строку"""
    def __init__(self, minsize=0, maxsize=None):
        self.minsize = minsize
        self.maxsize = maxsize
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name, '')
    
    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError(f"{self.name} must be a string")
        if len(value) < self.minsize:
            raise ValueError(f"{self.name} is too short")
        if self.maxsize and len(value) > self.maxsize:
            raise ValueError(f"{self.name} is too long")
        obj.__dict__[self.name] = value
    
    def __set_name__(self, owner, name):
        self.name = name  # Автоматически получаем имя атрибута

class User:
    username = ValidatedString(minsize=3, maxsize=20)
    email = ValidatedString(minsize=5, maxsize=100)

user = User()
user.username = "alice"  # OK
print(user.username)     # alice

try:
    user.username = "ab"  # Слишком короткое
except ValueError as e:
    print(e)  # username is too short

4. Кеширующий дескриптор

class Cached:
    """Кеширует результат вычисления"""
    def __init__(self, func):
        self.func = func
        self.cache = {}
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        
        obj_id = id(obj)
        if obj_id not in self.cache:
            print(f"Computing {self.func.__name__}...")
            self.cache[obj_id] = self.func(obj)
        else:
            print(f"Using cached {self.func.__name__}")
        
        return self.cache[obj_id]

class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @Cached
    def area(self):
        import math
        return math.pi * self.radius ** 2

c = Circle(5)
print(c.area)  # Computing area...
print(c.area)  # Using cached area

5. Дескриптор для типизации

class TypedProperty:
    """Дескриптор, проверяющий тип значения"""
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)
    
    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(
                f"{self.name} must be {self.expected_type.__name__}, "
                f"got {type(value).__name__}"
            )
        obj.__dict__[self.name] = value

class Student:
    name = TypedProperty('name', str)
    age = TypedProperty('age', int)
    gpa = TypedProperty('gpa', float)

student = Student()
student.name = "Alice"
student.age = 20
student.gpa = 3.8

try:
    student.age = "twenty"  # Error
except TypeError as e:
    print(e)  # age must be int, got str

6. Дескриптор для логирования доступа

class Logged:
    """Логирует доступ к атрибуту"""
    def __init__(self):
        self.value = None
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        print(f"Getting {self.name}")
        return self.value
    
    def __set__(self, obj, value):
        print(f"Setting {self.name} = {value}")
        self.value = value
    
    def __set_name__(self, owner, name):
        self.name = name

class BankAccount:
    balance = Logged()

account = BankAccount()
account.balance = 1000  # Setting balance = 1000
print(account.balance)  # Getting balance

7. Дескриптор для method binding

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

class Calculator:
    @BoundMethod
    def add(self, a, b):
        return a + b

calc = Calculator()
print(calc.add(2, 3))  # 5

8. @property как дескриптор

# property в Python — это встроенный дескриптор
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    
    @property  # __get__
    def celsius(self):
        return self._celsius
    
    @celsius.setter  # __set__
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero")
        self._celsius = value
    
    @celsius.deleter  # __delete__
    def celsius(self):
        del self._celsius
    
    @property
    def fahrenheit(self):  # read-only
        return self._celsius * 9/5 + 32

t = Temperature(25)
print(t.celsius)      # 25
print(t.fahrenheit)   # 77.0
t.celsius = 30        # Использует setter
print(t.celsius)      # 30

9. Дескриптор с аннотациями

class Annotation:
    """Дескриптор, использующий type hints"""
    def __set_name__(self, owner, name):
        self.name = name
        self.expected_type = owner.__annotations__.get(name)
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)
    
    def __set__(self, obj, value):
        if self.expected_type:
            if not isinstance(value, self.expected_type):
                raise TypeError(
                    f"{self.name}: expected {self.expected_type}, "
                    f"got {type(value)}"
                )
        obj.__dict__[self.name] = value

class Book:
    title: str = Annotation()
    author: str = Annotation()
    pages: int = Annotation()

book = Book()
book.title = "The Great Gatsby"
book.author = "F. Scott Fitzgerald"
book.pages = 180

print(f"{book.title} by {book.author}")

10. Best practices

# ✓ Хорошо: Data дескриптор (с __set__)
class ValidatedEmail:
    def __get__(self, obj, objtype=None):
        return obj.__dict__.get('email')
    
    def __set__(self, obj, value):
        if '@' not in value:
            raise ValueError("Invalid email")
        obj.__dict__['email'] = value

# ✓ Хорошо: Используй __set_name__ для автоматического имени
class Descriptor:
    def __set_name__(self, owner, name):
        self.name = name

# ✓ Хорошо: Используй @property вместо дескриптора когда это проще
class Point:
    @property
    def x(self):
        return self._x

# ✗ Плохо: Слишком сложный дескриптор
class OverkillDescriptor:
    def __get__(self, obj, objtype=None):
        # Огромная логика здесь
        pass

Заключение

Дескрипторы — это мощный инструмент для контроля доступа к атрибутам. Используй их для валидации, логирования, кеширования и типизации. Но помни: в большинстве случаев @property проще и понятнее!