Дескрипторы — одна из тех фич Python, о которых многие слышали, но мало кто использует напрямую. При этом они лежат в основе @property, @classmethod, @staticmethod, слотов и даже обычного доступа к методам. Разберём, что такое дескрипторы, как их писать и когда они реально полезны
Дескрипторы — одна из тех фич Python, о которых многие слышали, но мало кто использует напрямую. При этом они лежат в основе @property, @classmethod, @staticmethod, слотов и даже обычного доступа к методам.
Разберём, что такое дескрипторы, как их писать и когда они реально полезны.
Что такое дескрипторДескриптор — это объект, который определяет, как происходит доступ к атрибуту другого объекта. Технически это любой объект, у которого есть хотя бы один из методов: get, set, delete.
Когда Python видит obj.attr, он не просто берёт значение из obj.__dict__. Происходит сложный протокол поиска, и если attr оказывается дескриптором — вызывается его метод.
Простейший пример:
class Verbose:
"""Дескриптор, который логирует доступ"""
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
value = obj.__dict__.get(self.name)
print(f"Getting {self.name}: {value}")
return value
def __set__(self, obj, value):
print(f"Setting {self.name} to {value}")
obj.__dict__[self.name] = value
class Person:
name = Verbose()
age = Verbose()
p = Person()
p.name = "Kolyan"
p.age = 30
print(p.name) При присваивании p.name = "Kolnya" Python видит, что Person.name — дескриптор с методом set, и вызывает Verbose.__set__(Person.name, p, "Alice").
Python различает data descriptors и non-data descriptors.
Data descriptor — имеет set и/или delete. Имеет высший приоритет.
Non-data descriptor — имеет только get. Уступает атрибутам экземпляра.
Порядок поиска атрибута obj.attr:
Data descriptor в классе или его родителях
Атрибут в obj.__dict__
Non-data descriptor в классе
Атрибут в классе
getattr (если определён)
Поэтому @property (data descriptor) нельзя переопределить в экземпляре, а обычный метод (non-data descriptor) можно:
class Example:
@property
def prop(self):
return "property"
def method(self):
return "method"
e = Example()
# аопытка переопределить property
e.__dict__['prop'] = "overridden"
print(e.prop) # "property" — data descriptor побеждает
# Переопределяем метод
e.__dict__['method'] = lambda: "overridden"
print(e.method()) # "overridden" — __dict__ побеждает non-data descriptor
Валидирующий дескрипторОдно из главных применений дескрипторов — валидация при присваивании. Вместо кучи property с одинаковой логикой:
class Validated:
"""Базовый валидирующий дескриптор"""
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = f'_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, None)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
def validate(self, value):
"""Переопределяется в наследниках"""
pass
class PositiveNumber(Validated):
def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError(f"{self.public_name} must be a number")
if value <= 0:
raise ValueError(f"{self.public_name} must be positive")
class NonEmptyString(Validated):
def validate(self, value):
if not isinstance(value, str):
raise TypeError(f"{self.public_name} must be a string")
if not value.strip():
raise ValueError(f"{self.public_name} cannot be empty")
class Product:
name = NonEmptyString()
price = PositiveNumber()
quantity = PositiveNumber()
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
# Использование
product = Product("Laptop", 999.99, 10)
product.price = -100 # ValueError: price must be positive
product.name = " " # ValueError: name cannot be emptyДескрипторы позволяют вынести повторяющуюся логику валидации в переиспользуемые компоненты. Один PositiveNumber — и все числовые поля во всех классах защищены.
До Python 3.6 дескрипторы не знали своего имени. Приходилось передавать его явно:
class OldWay:
name = Descriptor('name') # Дублирование
age = Descriptor('age')__set_name__(self, owner, name) вызывается автоматически при создании класса. owner класс, в котором дескриптор определён, name имя атрибута.
class AutoNamed:
def __set_name__(self, owner, name):
print(f"I am {name} in {owner.__name__}")
self.name = name
class MyClass:
foo = AutoNamed() # I am foo in MyClass
bar = AutoNamed() # I am bar in MyClassКак работает @property@property — это просто встроенный дескриптор. Можно реализовать его самостоятельно:
class MyProperty:
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc if doc else (fget.__doc__ if fget else None)
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
class Circle:
def __init__(self, radius):
self._radius = radius
@MyProperty
def radius(self):
"""Radius of the circle"""
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
c = Circle(5)
print(c.radius) # 5
c.radius = 10 # OK
c.radius = -1 # ValueErrorМетоды getter, setter, deleter возвращают новый экземпляр дескриптора с обновлённым соответствующим методом. Так можно использовать декораторную цепочку @radius.setter.
Обычные методы — тоже дескрипторы. Функция в Python — non-data descriptor:
def func(self):
return "I'm a method"
print(type(func).__get__) # <slot wrapper '__get__' ...>Когда вы обращаетесь к obj.method, Python вызывает type(obj).__dict__['method'].__get__(obj, type(obj)). Функция возвращает bound method — объект, который хранит ссылку на obj и вызывает функцию с ним как первым аргументом.
class Demo:
def method(self):
return f"Called on {self}"
d = Demo()
# Это эквивалентно:
d.method()
Demo.__dict__['method'].__get__(d, Demo)()@staticmethod и @classmethod — тоже дескрипторы, просто с другой логикой get:
class StaticMethod:
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
return self.func # Возвращаем функцию как есть
class ClassMethod:
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
if objtype is None:
objtype = type(obj)
# Возвращаем bound method, но привязанный к классу
return self.func.__get__(objtype, type(objtype))Ленивые вычисления с кешированиемПопулярный паттерн — вычислить значение один раз и закешировать:
class cached_property:
"""Вычисляет значение при первом доступе, потом берёт из кеша"""
def __init__(self, func):
self.func = func
self.attrname = None
self.__doc__ = func.__doc__
def __set_name__(self, owner, name):
self.attrname = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
# Проверяем кеш в __dict__ экземпляра
cache = obj.__dict__
if self.attrname not in cache:
cache[self.attrname] = self.func(obj)
return cache[self.attrname]
class DataProcessor:
def __init__(self, data):
self.data = data
@cached_property
def processed(self):
print("Processing... (expensive operation)")
return [x * 2 for x in self.data]
dp = DataProcessor([1, 2, 3, 4, 5])
print(dp.processed) # Processing.. [2, 4, 6, 8, 10]
print(dp.processed) # [2, 4, 6, 8, 10] без Processing
print(dp.processed) # [2, 4, 6, 8, 10] снова из кешаЭто non-data descriptor. При первом вызове значение сохраняется в obj.__dict__, и при следующих обращениях dict имеет приоритет над non-data дескриптором.
Вообще, щас в питоне есть встроенный functools.cached_property, но понимание механизма полезно.
slots — механизм экономии памяти, который заменяет dict на фиксированный набор атрибутов. slots реализованы через дескрипторы:
class WithSlots:
__slots__ = ('x', 'y')
# Python создаёт дескрипторы автоматически
print(type(WithSlots.x)) # <class 'member_descriptor'>member_descriptor — это C-реализация дескриптора, который читает и пишет по фиксированному смещению в памяти объекта.
Дескрипторы хорошо сочетаются с type hints для создания строго типизированных атрибутов:
from typing import Generic, TypeVar, Type, Any, get_type_hints
T = TypeVar('T')
class TypedAttribute(Generic[T]):
def __set_name__(self, owner: Type, name: str):
self.name = name
self.private_name = f'_{name}'
# Получаем тип из аннотации
hints = get_type_hints(owner)
self.expected_type = hints.get(name)
def __get__(self, obj: Any, objtype: Type = None) -> T:
if obj is None:
return self # type: ignore
return getattr(obj, self.private_name)
def __set__(self, obj: Any, value: T) -> None:
if self.expected_type and not isinstance(value, self.expected_type):
raise TypeError(
f"{self.name} must be {self.expected_type.__name__}, "
f"got {type(value).__name__}"
)
setattr(obj, self.private_name, value)
def typed_attributes(cls):
"""Декоратор класса: заменяет аннотированные атрибуты на TypedAttribute"""
hints = get_type_hints(cls)
for name, type_hint in hints.items():
if not hasattr(cls, name) or isinstance(getattr(cls, name), TypedAttribute):
setattr(cls, name, TypedAttribute())
return cls
@typed_attributes
class User:
name: str
age: int
email: str
def __init__(self, name: str, age: int, email: str):
self.name = name
self.age = age
self.email = email
user = User("Alice", 30, "alice@example.com")
user.age = "thirty" # TypeError: age must be int, got strЭто простая рантайм проверка типов на основе аннотаций. Для прода же лучше использовать pydantic или attrs.
Когда используем дескрипторыХороший выбор:
Переиспользуемая логика доступа к атрибутам (то есть всякая валидация, преобразование, логирование)
Ленивые вычисления с кешированием
ORM-поля (SQLAlchemy, Django ORM — построены на дескрипторах)
Создание DSL и декларативных API
Перебор:
Простая валидация в одном классе — хватит @property
Один-два атрибута, не стоит заводить инфраструктуру
Когда dataclasses или attrs решают задачу проще
Хранение данных в дескрипторе:
class Wrong:
def __init__(self):
self.value = None # Shared между всеми экземплярами
def __get__(self, obj, objtype=None):
return self.value
def __set__(self, obj, value):
self.value = value # Перезаписывает для всех
class MyClass:
attr = Wrong()
a = MyClass()
b = MyClass()
a.attr = 1
print(b.attr) # 1Данные нужно хранить в экземпляре (obj.__dict__ или setattr(obj, ...)), не в дескрипторе.
None при доступе через класс:
class Descriptor:
def __get__(self, obj, objtype=None):
if obj is None:
return self # важно!!
return obj.__dict__.get('value')При MyClass.attr (без экземпляра) obj будет None. Если не обработать, получите AttributeError или странное поведение.
Итак, если @property или dataclass решают задачу — используйте их. Дескрипторы нужны, когда логика действительно переиспользуется и достаточно сложна, чтобы оправдать абстракцию.

Если хочется не просто «пощупать» дескрипторы, а системно прокачать базу Python-разработчика, посмотрите программу Python Developer. Basic: синтаксис, работа с БД, API, асинхронщина и веб на Django/FastAPI. На выходе — практические навыки и первые проекты в портфолио. Пройдите входное тестирование и получите скидку на курс.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Строки в Python: всё не так просто, как кажется | 0 | 6.22 | 19-05-2026 |
| 2 | Контекстные менеджеры в Python за пределами with open(): пишем свои и упрощаем код | 0 | 6.22 | 01-06-2026 |
| 3 | Разработка на Python: когда много думать = вредить проекту | 0 | 10.08 | 02-06-2026 |
| 4 | Как устроен словарь в CPython: compact dict, key sharing и что с ним делает free-threading | 0 | 12.25 | 09-05-2026 |
| 5 | Скрытая цена словарей Python и три более безопасные альтернативы | 0 | 9.22 | 15-01-2026 |
| 6 | The Hidden Cost of Python Dictionaries | 0 | 8.52 | 29-01-2026 |
| 7 | Связывание абстрактных классов со свойствами | 0 | 7.66 | 24-02-2026 |
| 8 | Объяснение лямбда исчисления через Python | 0 | 7.94 | 22-04-2026 |
| 9 | Практики разработки на Python | 0 | 5.95 | 29-03-2026 |
| 10 | DTO, schema, model, entity: почему в коде всё называется User | 0 | 7 | 01-07-2026 |