Slots в классах: оптимизация памяти и ограничение атрибутов
Что такое slots и зачем они нужны? 🧐
Когда вы создаёте обычный класс в Python, каждый его экземпляр хранит атрибуты в динамическом словаре __dict__. Это удобно — можно добавлять любые атрибуты на лету! Но за гибкость приходится платить:
- Потребление памяти: Каждый
__dict__занимает дополнительное место - Скорость доступа: Поиск в словаре медленнее, чем прямой доступ к атрибутам
Вот тут на сцену выходят __slots__ — специальный механизм, который:
- Жёстко фиксирует набор возможных атрибутов класса
- Заменяет
__dict__на более компактное хранение - Ускоряет доступ к атрибутам
class RegularClass:
pass
obj = RegularClass()
obj.new_attr = 42 # Можно добавить что угодно
class SlotClass:
__slots__ = ['x', 'y'] # Фиксированный набор атрибутов
slot_obj = SlotClass()
slot_obj.x = 10
# slot_obj.z = 20 # Вызовет AttributeError!
Как работают slots под капотом 🔧
Когда вы определяете __slots__, Python:
- Создаёт дескрипторы для каждого указанного атрибута
- Выделяет фиксированный массив для хранения значений
- Отказывается от создания
__dict__(если не добавить его явно в__slots__)
class Point:
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
Память экономится за счёт:
- Отсутствия хэш-таблицы (
__dict__) - Хранения значений в плотном массиве вместо словаря
- Отсутствия
__weakref__(если не указан в__slots__)
Замеряем эффективность ⚡
Давайте сравним обычный класс и класс со слотами:
from sys import getsizeof
class Regular:
def __init__(self, x, y):
self.x = x
self.y = y
class Slot:
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
# Сравниваем размер в памяти
r = Regular(1, 2)
s = Slot(1, 2)
print(getsizeof(r)) # Обычно 56 байт
print(getsizeof(s)) # Обычно 32 байта
На практике экономия становится заметной при создании тысяч экземпляров:
from memory_profiler import profile
@profile
def create_many_regular():
return [Regular(i, i+1) for i in range(100000)]
@profile
def create_many_slot():
return [Slot(i, i+1) for i in range(100000)]
Когда использовать slots? 🤔
Идеальные кандидаты:
- Классы, которых будет много экземпляров (например, узлы в графе)
- Объекты с фиксированным набором атрибутов
- Ситуации, где важна производительность
Не лучший выбор:
- Классы, которым нужна динамичность (добавление атрибутов)
- Когда используется множественное наследование со сложной структурой
# Хороший пример — immutable объект
class Vector2D:
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
Продвинутые возможности 🚀
1. Наследование и slots
При наследовании __slots__ ведут себя особым образом:
class Parent:
__slots__ = ['x']
class Child(Parent):
__slots__ = ['y'] # Дополняет родительские слоты
obj = Child()
obj.x = 1
obj.y = 2
2. Добавление dict
Если вам нужна частичная динамичность:
class Mixed:
__slots__ = ['x', '__dict__']
def __init__(self, x):
self.x = x
m = Mixed(10)
m.y = 20 # Работает, так как есть __dict__
3. Дескрипторы и свойства
@property отлично сочетается с __slots__:
class Temperature:
__slots__ = ['_celsius']
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
def __init__(self, celsius):
self._celsius = celsius
Ограничения и подводные камни ⚠️
1. Нельзя добавлять новые атрибуты
class Strict:
__slots__ = ['name']
s = Strict()
s.name = "Python"
# s.age = 30 # AttributeError!
2. Сложности с pickle и copy
- Может потребоваться реализация __getstate__/__setstate__
3. Не работает с некоторыми ORM - Например, Django модели несовместимы со слотами
Итоговые рекомендации 🏆
- Используйте
__slots__для стабильных по структуре классов с большим количеством экземпляров - Измеряйте производительность — иногда прирост не стоит ограничений
- Помните про наследование — дочерние классы могут расширять
__slots__ - Для частичной динамичности добавляйте
__dict__в__slots__
# Оптимальный вариант для высоконагруженных объектов
class Particle:
__slots__ = ['mass', 'position', 'velocity']
def __init__(self, mass, pos, vel):
self.mass = mass
self.position = pos
self.velocity = vel
Теперь вы знаете мощный инструмент оптимизации! Применяйте его с умом, и ваши программы станут быстрее и экономнее. 🚀