__slots__ в Python: сколько памяти экономит и что ломает
На миллионе объектов __slots__ срезает потребление памяти в разы — но ломает динамические атрибуты, weakref и часть паттернов наследования. Разбираем, когда выигрыш реален, а когда вы теряете гибкость впустую.

Список из миллиона экземпляров обычного класса с тремя атрибутами легко съедает несколько сотен мегабайт — заметная часть уходит не на данные, а на служебный __dict__ каждого объекта. Добавление одной строки __slots__ в класс убирает этот словарь и сокращает потребление в разы. Но у экономии есть цена: часть привычного поведения объектов ломается, и не всегда об этом сообщают явной ошибкой. Разбираем на CPython 3.12, где что именно вы выигрываете и чем платите.
Откуда берётся экономия
По умолчанию каждый экземпляр Python-класса хранит атрибуты в словаре __dict__. Словарь — гибкая структура: вы можете в любой момент присвоить объекту новый атрибут. Но за гибкость платите памятью: пустой словарь в CPython — это отдельный объект с собственным заголовком, таблицей хешей и запасом под рост.
Когда вы объявляете __slots__, интерпретатор перестаёт создавать __dict__ для экземпляров и вместо этого выделяет фиксированные ячейки под перечисленные имена — как поля в структуре C. Доступ к ним идёт через дескрипторы, а не через хеш-таблицу.
# Python 3.12
class PointDict:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
class PointSlots:
__slots__ = ('x', 'y', 'z')
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
Замерить разницу на своей машине можно так — важно мерить именно суммарный размер пула объектов, а не один экземпляр:
import sys
import tracemalloc
def measure(cls, count=1_000_000):
tracemalloc.start()
# держим ссылки, иначе сборщик уберёт объекты
objects = [cls(i, i, i) for i in range(count)]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return current, len(objects)
cur_dict, _ = measure(PointDict)
cur_slots, _ = measure(PointSlots)
print(f'dict: {cur_dict / 1024 / 1024:.1f} MB')
print(f'slots: {cur_slots / 1024 / 1024:.1f} MB')
Точные цифры зависят от версии CPython, разрядности и количества атрибутов. Не переписывайте чужие бенчмарки как факт — прогоните tracemalloc на своей конфигурации. Порядок эффекта: для класса с несколькими простыми полями __slots__ убирает основную долю накладных расходов на экземпляр. Чем больше объектов и меньше атрибутов, тем заметнее выигрыш в относительных числах.
Экономия памяти от __slots__ растёт с числом экземпляров, а не с числом атрибутов. Десяток объектов — оптимизировать нечего. Миллион однотипных записей — уже разговор о сотнях мегабайт.
Когда это реально имеет смысл
Порог принятия решения простой: слоты оправданы, когда объектов много и они живут одновременно.
- Массивы однотипных мелких объектов. Точки, узлы графа, строки распарсенного лога, записи из большого CSV — миллионы штук в памяти.
- Долгоживущие структуры данных. Дерево или связный список из сотен тысяч узлов, который висит в памяти всё время работы процесса.
- Классы с фиксированным набором полей. Если атрибуты объекта известны заранее и не меняются — слоты заодно защищают от опечаток в имени атрибута.
И где смысла нет: единичные сервисные объекты, конфиги, контроллеры, синглтоны. Там __dict__ ничего не стоит, а слоты только отберут гибкость.
Что ломается
Динамическое присвоение атрибутов
Главное последствие: объект со слотами не принимает атрибуты, которых нет в списке. Попытка присвоить лишнее падает с AttributeError.
p = PointSlots(1, 2, 3)
p.color = 'red' # AttributeError: 'PointSlots' object has no attribute 'color'
Для одних это баг, для других — фича: слоты работают как контракт. Но библиотеки и код, которые рассчитывают дописать служебный атрибут вашему объекту (кеши, метки, monkey-patching), сломаются.
Пропало weakref
Обычный объект по умолчанию поддерживает слабые ссылки через weakref. У объекта со слотами этой возможности нет, если вы явно не добавили '__weakref__' в список слотов.
import weakref
class Node:
__slots__ = ('value', '__weakref__') # без __weakref__ упадёт
def __init__(self, value):
self.value = value
n = Node(42)
ref = weakref.ref(n) # теперь работает
Забыть об этом легко: код с weakref обычно живёт в другом модуле, и ошибка вылезет далеко от определения класса.
Наследование съедает экономию
Слоты действуют только на тот класс, где объявлены. Если хотя бы один класс в цепочке наследования не задал __slots__, экземпляры снова получают __dict__, и вся экономия исчезает — при этом ошибки не будет, объект просто станет тяжёлым.
class Base:
__slots__ = ('a',)
class Child(Base):
pass # нет __slots__ -> у экземпляров снова появляется __dict__
c = Child()
c.whatever = 1 # проходит без ошибки, экономии больше нет
Чтобы слоты работали по всей цепочке, __slots__ должен быть в каждом классе. В подклассе перечисляйте только новые атрибуты — дублировать унаследованные не нужно и вредно (получите дублирующиеся дескрипторы).
Конфликт с другими механизмами
- Значения по умолчанию на уровне класса. Нельзя объявить атрибут в
__slots__и одновременно задать ему значение как переменной класса — имена конфликтуют, будетValueErrorпри определении класса. - Множественное наследование. Два родителя с непустыми
__slots__вызывают конфликт компоновки, если у обоих непустые слоты — Python подниметTypeErrorпри создании класса. - Пиклинг. Объекты со слотами сериализуются
pickle, но при кастомном__getstate__/__setstate__отсутствие__dict__нужно учитывать вручную.
Слоты и dataclasses
С Python 3.10 декоратор @dataclass принимает параметр slots=True — он сам сгенерирует __slots__ из аннотаций полей. Это самый безопасный способ получить слоты, не выписывая имена руками и не рассинхронизировав их с полями.
from dataclasses import dataclass
@dataclass(slots=True) # доступно с Python 3.10
class Point:
x: int
y: int
z: int
Нюанс: slots=True заставляет декоратор пересоздать класс (нельзя добавить слоты к уже определённому классу без пересоздания). Если у вас есть ссылки на исходный класс или сложные декораторы поверх — проверьте, что всё указывает на итоговый класс. На версиях ниже 3.10 этого параметра нет, слоты придётся писать вручную.
Таблица: чем платите за экономию
| Возможность обычного класса | С __slots__ | Как вернуть |
|---|---|---|
| Присвоить новый атрибут на лету | AttributeError | Добавить '__dict__' в слоты (теряется экономия) |
| Слабые ссылки (weakref) | Не работает | Добавить '__weakref__' в слоты |
| Наследование без забот | __slots__ нужен в каждом классе | Прописать слоты по всей цепочке |
| Значение поля по умолчанию как переменная класса | ValueError при определении | Задавать значение в __init__ |
Что делать дальше
Не расставляйте __slots__ превентивно по всему проекту — это преждевременная оптимизация, которая отберёт гибкость там, где памяти и так хватает. Порядок действий:
- Найдите профилировщиком памяти (
tracemalloc,memray) классы, которые реально занимают много — обычно это несколько типов с миллионами экземпляров. - Добавьте слоты только им, через
@dataclass(slots=True)если проект на 3.10+. - Прогоните тесты: слоты чаще всего вскрывают код, который дописывал объектам служебные атрибуты.
- Повторно замерьте память в тех же условиях — на том же объёме данных и той же версии Python.
Инвалидация ссылок между версиями и поведение pickle со слотами — отдельная тема; здесь мы разобрали только экономию памяти и её цену. Если после включения слотов память не упала — скорее всего __dict__ вернулся через незакрытую цепочку наследования, начните проверку оттуда.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Python Data model: __slots__ (docs.python.org), dataclasses — slots parameter (docs.python.org)
Частые вопросы
У меня Python 3.9, параметр slots=True в dataclass не работает. Что делать?
Я добавил __slots__, но память не уменьшилась. Почему?
Сколько именно памяти экономит __slots__?
Ломает ли __slots__ наследование?
Можно ли иногда всё-таки присваивать объекту лишние атрибуты?
Работает ли weakref с объектами, у которых есть __slots__?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.