Slots в классах: оптимизация памяти и ограничение атрибутов

Что такое slots и зачем они нужны? 🧐

Когда вы создаёте обычный класс в Python, каждый его экземпляр хранит атрибуты в динамическом словаре __dict__. Это удобно — можно добавлять любые атрибуты на лету! Но за гибкость приходится платить:

  1. Потребление памяти: Каждый __dict__ занимает дополнительное место
  2. Скорость доступа: Поиск в словаре медленнее, чем прямой доступ к атрибутам

Вот тут на сцену выходят __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:

  1. Создаёт дескрипторы для каждого указанного атрибута
  2. Выделяет фиксированный массив для хранения значений
  3. Отказывается от создания __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 модели несовместимы со слотами


Итоговые рекомендации 🏆

  1. Используйте __slots__ для стабильных по структуре классов с большим количеством экземпляров
  2. Измеряйте производительность — иногда прирост не стоит ограничений
  3. Помните про наследование — дочерние классы могут расширять __slots__
  4. Для частичной динамичности добавляйте __dict__ в __slots__
# Оптимальный вариант для высоконагруженных объектов
class Particle:
    __slots__ = ['mass', 'position', 'velocity']

    def __init__(self, mass, pos, vel):
        self.mass = mass
        self.position = pos
        self.velocity = vel

Теперь вы знаете мощный инструмент оптимизации! Применяйте его с умом, и ваши программы станут быстрее и экономнее. 🚀

Скрыть рекламу навсегда

🧠 Учёба без воды и зубрёжки

Закрытый Boosty с наработками опытного преподавателя.

Объясняю сложное так, чтобы щелкнуло.

🚀 Забрать доступ к Boosty