Дескрипторы и __slots__
Что такое дескриптор
Дескриптор — объект, который лежит в атрибуте класса и управляет доступом к этому атрибуту у экземпляров. Достаточно определить хотя бы один метод протокола:
| Метод | Вызывается при |
|---|---|
__get__(self, instance, owner) |
чтении obj.attr (и Class.attr, тогда instance is None) |
__set__(self, instance, value) |
присваивании obj.attr = value |
__delete__(self, instance) |
del obj.attr |
__set_name__(self, owner, name) |
создании класса: дескриптор узнаёт своё имя |
class Loud:
def __set_name__(self, owner, name):
print(f" __set_name__: класс {owner.__name__}, атрибут {name!r}")
self.name = name
def __get__(self, instance, owner):
print(f" __get__: instance={instance!r:.30}")
if instance is None:
return self
return instance.__dict__.get(self.name, "<пусто>").upper()
def __set__(self, instance, value):
print(f" __set__: {value!r}")
instance.__dict__[self.name] = value
class User:
nickname = Loud() # здесь сработает __set_name__
u = User()
u.nickname = "neo"
print(u.nickname)
print(User.nickname)
Дескриптор-валидатор
Классический пример — проверка полей, как в ORM (Django models.Field, SQLAlchemy Column):
class Typed:
def __init__(self, kind, *, min_value=None):
self.kind = kind
self.min_value = min_value
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, self.kind):
raise TypeError(f"{self.name}: ожидался {self.kind.__name__}, получен {type(value).__name__}")
if self.min_value is not None and value < self.min_value:
raise ValueError(f"{self.name}: меньше {self.min_value}")
instance.__dict__[self.name] = value
class Product:
name = Typed(str)
price = Typed(float, min_value=0.0)
qty = Typed(int, min_value=0)
def __init__(self, name, price, qty):
self.name, self.price, self.qty = name, price, qty
p = Product("Кофе", 199.0, 3)
print(p.name, p.price, p.qty, p.__dict__)
for bad in [("Чай", "дёшево", 1), ("Чай", 50.0, -1)]:
try:
Product(*bad)
except (TypeError, ValueError) as e:
print(type(e).__name__, e)
Один класс Typed переиспользуется для всех полей — в отличие от property, который пришлось бы писать для каждого поля отдельно.
Дескрипторы данных и не-данных
- Дескриптор данных — есть
__set__или__delete__(например,property). - Дескриптор не-данных — только
__get__(например, функции,classmethod,staticmethod).
От этого зависит приоритет при чтении obj.attr:
class DataDesc:
def __get__(self, obj, owner): return "data descriptor"
def __set__(self, obj, value): pass
class NonDataDesc:
def __get__(self, obj, owner): return "non-data descriptor"
class A:
d = DataDesc()
n = NonDataDesc()
a = A()
a.__dict__["d"] = "instance dict"
a.__dict__["n"] = "instance dict"
print(a.d) # дескриптор данных сильнее __dict__
print(a.n) # __dict__ сильнее дескриптора не-данных
Поэтому cached_property (не-данных) работает: первый раз вычисляет и кладёт значение в __dict__, а потом __dict__ перекрывает дескриптор.
Методы — тоже дескрипторы
class Greeter:
def hello(self, name):
return f"Привет, {name}"
g = Greeter()
f = Greeter.__dict__["hello"] # «сырая» функция в словаре класса
print(type(f).__name__, hasattr(f, "__get__"))
bound = f.__get__(g, Greeter) # именно это делает Python при g.hello
print(bound, bound("Анна"))
print(Greeter.__dict__["hello"] is Greeter.hello) # через класс — та же функция
property, classmethod, staticmethod, functools.cached_property — всё это дескрипторы, написанные на C или Python.
slots
По умолчанию атрибуты экземпляра хранятся в словаре __dict__ — гибко, но расточительно. __slots__ заменяет словарь фиксированными ячейками:
import sys
class WithDict:
def __init__(self, x, y):
self.x, self.y = x, y
class WithSlots:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
a, b = WithDict(1, 2), WithSlots(1, 2)
print("с __dict__:", sys.getsizeof(a) + sys.getsizeof(a.__dict__), "байт")
print("со __slots__:", sys.getsizeof(b), "байт")
print(hasattr(b, "__dict__"), type(WithSlots.x).__name__) # каждый слот — дескриптор!
try:
b.z = 3
except AttributeError as e:
print("AttributeError:", e)
Когда полезно: миллионы мелких объектов (точки, узлы графа, записи из БД) — экономия памяти до 40–50% и чуть более быстрый доступ.
Ограничения slots
- Нельзя добавлять новые атрибуты (иногда это плюс — опечатка
self.nmaeсразу даст ошибку). - Наследник без своих
__slots__снова получает__dict__. - Для слабых ссылок нужен слот
"__weakref__". - В dataclass проще всего:
@dataclass(slots=True).
getattribute и getattr
__getattribute__вызывается при каждом обращении к атрибуту — именно он реализует алгоритм выше. Переопределять его опасно и редко нужно.__getattr__вызывается, только когда атрибут не найден обычным путём — удобен для прокси и значений по умолчанию.
class Proxy:
def __init__(self, target):
self._target = target
self.calls = 0
def __getattr__(self, name): # только для отсутствующих атрибутов
self.calls += 1
return getattr(self._target, name)
p = Proxy([3, 1, 2])
p.append(0)
p.sort()
print(p._target, p.calls) # _target и calls нашлись обычным путём
Вопрос на собеседовании
«Как работает property внутри?» — property — это дескриптор данных: класс с методами __get__, __set__ и __delete__, которые вызывают переданные функции fget/fset/fdel. Поскольку это дескриптор данных, он имеет приоритет над __dict__ экземпляра.
Итог
- Дескриптор — объект в атрибуте класса с
__get__/__set__/__delete__;__set_name__сообщает ему имя. - Дескриптор данных (есть
__set__) сильнее__dict__экземпляра, дескриптор не-данных — слабее. - Функции,
property,classmethod,staticmethod— дескрипторы. __slots__убирает__dict__, экономит память и запрещает новые атрибуты.__getattr__— для отсутствующих атрибутов,__getattribute__— для всех.
Практика решено 0 из 5
Напишите дескриптор Positive, который разрешает присваивать атрибуту только положительные числа
(иначе ValueError). Имя атрибута узнайте в __set_name__ и храните значение в instance.__dict__
под этим именем.
Класс Order уже использует его для полей qty и price.
Какие тесты проверяют код (5)
o = Order(2, 100); assert (o.qty, o.price) == (2, 100)
o = Order(1, 1)
try:
o.qty = 0
ok = False
except ValueError:
ok = True
assert ok, "qty = 0 должно давать ValueError"
try:
Order(1, -5)
ok = False
except ValueError:
ok = True
assert ok, "Отрицательная цена должна давать ValueError"
a, b = Order(1, 10), Order(2, 20); assert (a.price, b.price) == (10, 20), "Значения должны храниться у каждого экземпляра отдельно"
assert isinstance(Order.qty, Positive), "При обращении через класс верните сам дескриптор"
class Positive:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
if value <= 0:
raise ValueError(f"{self.name} должно быть положительным")
instance.__dict__[self.name] = value
class Order:
qty = Positive()
price = Positive()
def __init__(self, qty, price):
self.qty = qty
self.price = price
Создайте класс Pixel с полями x, y, color, у которого есть __slots__,
чтобы у объектов не было __dict__ и нельзя было добавить посторонний атрибут.
Какие тесты проверяют код (3)
p = Pixel(1, 2, "red"); assert (p.x, p.y, p.color) == (1, 2, "red")
assert not hasattr(Pixel(0, 0, "x"), "__dict__"), "У объекта со __slots__ нет __dict__"
p = Pixel(0, 0, "x")
try:
p.alpha = 1
ok = False
except AttributeError:
ok = True
assert ok, "Посторонний атрибут добавить нельзя"
class Pixel:
__slots__ = ("x", "y", "color")
def __init__(self, x, y, color):
self.x, self.y, self.color = x, y, color
Что выведет код?
class D:
def __get__(self, obj, owner):
return "из дескриптора"
class A:
x = D()
a = A()
a.__dict__["x"] = "из __dict__"
print(a.x)
Как обычная функция в классе становится связанным методом при обращении obj.method?
Что верно про __slots__?
Войдите, чтобы отмечать пройденные уроки.