Что такое `__slots__` в классах Python и для чего он используется?

Ответ

__slots__ в Python — это специальный атрибут класса, который позволяет явно объявить набор допустимых атрибутов для экземпляров этого класса. Его основное назначение — оптимизация использования памяти и, в некоторых случаях, ускорение доступа к атрибутам.

Как это работает: По умолчанию каждый экземпляр класса в Python имеет словарь __dict__, который хранит все его атрибуты. Этот словарь занимает дополнительную память. Когда вы определяете __slots__, Python вместо __dict__ резервирует фиксированное пространство для каждого атрибута, указанного в __slots__, что значительно сокращает потребление памяти, особенно для классов с большим количеством экземпляров. Также __slots__ предотвращает создание __weakref__ для экземпляров, если он не указан явно в __slots__.

Преимущества:

  • Экономия памяти: Значительное сокращение потребления памяти, так как экземпляры не хранят __dict__. Это особенно критично для приложений, создающих миллионы объектов.
  • Ускорение доступа к атрибутам: Доступ к атрибутам через __slots__ может быть быстрее, чем через __dict__, поскольку Python не нужно выполнять поиск в словаре.

Ограничения и особенности:

  • Невозможность динамического добавления атрибутов: После определения __slots__ вы не сможете добавлять новые атрибуты к экземплярам, кроме тех, что перечислены. Попытка сделать это вызовет AttributeError.
  • Наследование:
    • Если дочерний класс не определяет __slots__, он будет иметь __dict__.
    • Если дочерний класс определяет __slots__, он должен включать атрибуты родительского __slots__ (или Python будет использовать __dict__ для родительских атрибутов).
    • Если вы хотите, чтобы экземпляры могли иметь __dict__ или __weakref__ в дополнение к __slots__, их нужно явно включить в список __slots__.
  • Множественное наследование: Может быть сложным, если несколько базовых классов имеют __slots__.

Пример:

class PersonWithSlots:
    __slots__ = ['name', 'age'] # Разрешенные атрибуты

    def __init__(self, name, age):
        self.name = name
        self.age = age

class PersonWithoutSlots:
    def __init__(self, name, age):
        self.name = name
        self.age = age

p_slots = PersonWithSlots("Alice", 30)
print(f"PersonWithSlots: {p_slots.name}, {p_slots.age}")
# print(p_slots.__dict__) # AttributeError: 'PersonWithSlots' object has no attribute '__dict__'

try:
    p_slots.job = "Engineer"
except AttributeError as e:
    print(f"Ошибка при добавлении нового атрибута: {e}")

p_no_slots = PersonWithoutSlots("Bob", 25)
print(f"PersonWithoutSlots: {p_no_slots.name}, {p_no_slots.age}")
print(f"__dict__ PersonWithoutSlots: {p_no_slots.__dict__}") # {'name': 'Bob', 'age': 25}
p_no_slots.job = "Developer" # OK
print(f"PersonWithoutSlots с новым атрибутом: {p_no_slots.job}")

Когда использовать __slots__: Используйте __slots__ только тогда, когда вы работаете с большим количеством экземпляров класса (тысячи или миллионы) и точно знаете, какие атрибуты будут у ваших объектов. В большинстве обычных случаев его использование не требуется и может усложнить код.

Ответ 18+ 🔞

А, блядь, __slots__! Ну это ж классика, ёпта, как Муму у Тургенева — с виду простая хуйня, а внутри трагедия и экономия памяти, в рот меня чих-пых!

Смотри, в чём прикол. Обычно, когда ты создаёшь объект в Python, он внутри себя таскает с собой словарик __dict__, как тот самый Герасим таскал свою Муму. И в этот словарик он складывает все свои атрибуты — имя, возраст, должность, любовницу, кредиты — всё, что в голову взбредёт. Удобно? Удобно. Но памяти жрёт, как здоровый немой татарин щи, — овердохуища!

А теперь представь, что у тебя не один объект, а миллион таких Герасимов. И у каждого свой личный словарик-Муму. Пиздец, да? Памяти не напасёшься.

Вот тут-то и выходит на сцену __slots__, как злая барыня, которая говорит: «Хуй вам, а не словарики!». Ты явно объявляешь, какие атрибуты твоим объектам можно иметь, и всё. Больше ни-ху-я.

class СуровыйМужик:
    __slots__ = ['имя', 'возраст', 'сила']  # Всё, блядь. Точка.

    def __init__(self, имя, возраст, сила):
        self.имя = имя
        self.возраст = возраст
        self.сила = сила

вася = СуровыйМужик("Вася", 35, "дохуя")
print(f"{вася.имя}, сила: {вася.сила}")  # Всё ок

# А теперь попробуй добавить ему любовницу...
try:
    вася.любовница = "Марина из бухгалтерии"
except AttributeError as e:
    print(f"Не, Вася, не выйдет: {e}")  # AttributeError, ёпта!

Видишь? Сказали, что у Васи только имя, возраст и сила — значит, только они. Хотел записать любовницу — получи в сраку AttributeError. Максимализм, блядь, как у того Герасима — либо так, либо нихуя.

Что хорошего-то?

  1. Памяти меньше жрёт. Вместо толстого словаря — компактная фиксированная структура. Для миллионов объектов — разница, блядь, как между «Муму» и «Война и мир».
  2. Быстрее работает. Доступ к атрибутам становится чуть шустрее, потому что не надо рыскать по словарю.

А подводные ебучки какие?

  • Динамически атрибуты не добавишь. Захотел на ходу прилепить объекту новое свойство — хуй тебе, а не свойство. Всё должно быть объявлено заранее.
  • С наследованием — головняк. Если родительский класс использует __slots__, а ты в дочернем про них забыл — у дочернего появится свой __dict__, и часть экономии памяти накроется медным тазом. Если и в дочернем объявишь — не забудь включить туда слоты родителя, а то нихуя не заработает.
  • Хочешь __dict__ или __weakref__? Проси explicitly, блядь! Включи их в список __slots__, если нужны.

Так когда это юзать-то?

Только тогда, когда ты реально создаёшь хуеву тучу одинаковых объектов (тысячи, миллионы) и точно-преточно знаешь, какие у них будут поля. В 95% обычного кода это overengineering, чистая ебля мозга. Но если пишешь какую-нибудь хайлоад систему, где каждый байт на счету — тогда да, __slots__ твой бро.

Короче, инструмент мощный, но с характером. Как тот самый Герасим — может и помочь, а может и утопить твою затею, если использовать бездумно.