Для начала небольшой дисклеймер.

Эта статья вдохновлена моим обучением. Когда я только начинал свой Python-way, на одном из форумов увидел новое для себя понятие - слоты. Но сколько я не искал, в сети было крайне мало статей на эту тему, поэтому понять и осознать слоты было достаточно сложно. Данная статья призвана помочь начинающим в этой теме, но даже опытные разработчики, уверен, найдут здесь нечто новое.


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

К счастью, есть выход — слоты, они обеспечивают специальный механизм уменьшения размера объектов. Это концепция оптимизации памяти на объектах. Также, использование слотов позволяет нам ускорить доступ к атрибутам.

Пример объекта python без слотов:

class NoSlots:
  def __init__(self):
    self.a = 1
    self.b = 2 


if __name__ == "__main__": 
  ns = NoSlots()
  print(ns.__dict__)

Выход:

{'a': 1, 'b': 2}

Поскольку каждый объект в Python содержит динамический словарь, который позволяет добавлять атрибуты. Для каждого объекта экземпляра у нас будет экземпляр словаря, который потребляет больше места и тратит много оперативной памяти. В Python нет функции по умолчанию для выделения статического объема памяти при создании объекта для хранения всех его атрибутов.

Использование slots уменьшает потери пространства и ускоряет работу программы, выделяя пространство для фиксированного количества атрибутов.

Пример объекта python со слотами:

class WithSlots(object):
  __slots__ = ['a', 'b']
  def __init__(self):
    self.a = 1
    self.b = 2

    
if __name__ == "__main__":
  ws = WithSlots()
  print(ws.__slots__) 

Выход:

['a', 'b']

Пример python, если мы используем dict:

class WithSlots:
  __slots__ = ['a', 'b']
  def init(self):
    self.a = 1
    self.b = 2

    
if __name__ == "__main__":
  ws = WithSlots()
  print(ws.__dict__) 

Выход:

AttributeError: объект WithSlots не имеет атрибута '__dict__'

Как мы видим, будет вызвана ошибка AttributeError. Не сложно догадаться, что раз мы не можем вызвать dict, то и создавать новые атрибуты мы не сможем.

Это что касается потребляемой памяти, а тем давайте рассмотрим скорость доступа к атрибутам:

Напишем небольшой тест:

class Foo(object): 
  __slots__ = ('foo',)

  
class Bar(object): 
  pass


def get_set_delete(obj):
  obj.foo = 'foo'
  obj.foo
  del obj.foo
  
def test_foo():  
  get_set_delete(Foo())
  
def test_bar():
  get_set_delete(Bar())

И с помощью модуля timeit оценим время выполнения:

>>> import timeit
>>> min(timeit.repeat(test_foo))
0.2567792439949699
>>> min(timeit.repeat(test_bar))
0.34515008199377917

Таким образом, получается, что класс с использованием slots примерно на 25-30 % быстрее на операциях доступа к атрибутам. Конечно, этот показатель может меняться в зависимости от версии языка или ОС на которой запускается программа.

Как мы видим, использовать слоты довольно просто, но есть и некоторые подводные камни. Например, наследование. Нужно понимать, что значение slots наследуется, однако это не предотвращает создание dict.

Таким образом, дочерние классы не будут запрещать добавлять динамические атрибуты, и добавляться они будут в__dict__, со всеми вытекающими расходами (по памяти и производительности).

class SlotsClass:
  __slots__ = ('foo', 'bar')

  
class ChildSlotsClass(SlotsClass):
  pass
>>> obj = ChildSlotsClass()
>>> obj.__slots__
('foo', 'bar')
>>> obj.foo = 5
>>> obj.something_new = 3
>>> obj.__dict__
{'something_new': 3}

Если нам нужно, чтобы и дочерний класс тоже был ограничен слотами, там придётся и в нём присвоить значение атрибуту slots. Кстати, дублировать уже указанные в родительском классе слоты не нужно.

class SlotsClass:
  __slots__ = ('foo', 'bar')

  
class ChildSlotsClass(SlotsClass):
  __slots__ = ('baz',)
>>> obj = ChildSlotsClass()
>>> obj.foo = 5py
>>> obj.baz = 6
>>> obj.something_new = 3
Traceback (most recent call last):
File "python", line 12, in <module>
AttributeError: 'ChildSlotsClass' object has no attribute 'something_new'

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

class BaseOne:
  __slots__ = ('param1',)

  
class BaseTwo:
  __slots__ = ('param2',)
>>> class Child(BaseOne, BaseTwo): __slots__ = ()

Выход:

Traceback (most recent call last):
File "<pyshell#68>", line 1, in <module>
class Child(BaseOne, BaseTwo): __slots__ = ()
TypeError: Error when calling the metaclass bases
multiple bases have instance lay-out conflict

Один из способов решения этой проблемы — абстрактные классы. Но об этом думаю поговорим в следующий раз.

Ну и под конец важные выводы:

  • Без переменной словаря dict, экземплярам нельзя назначить атрибуты, не указанные в определении slots. При попытке присвоения имени переменной, не указанной в списке, вы получите ошибку AttributeError. Если требуется динамическое присвоение новых переменных, добавьте значение 'dict' в объявлении атрибута slots.

  • Атрибуты slots, объявленные в родительских классах, доступны в дочерних классах. Однако дочерние подклассы получат dict, если они не переопределяют slots.

  • Если класс определяет слот, также определенный в базовом классе, переменная экземпляра, определенная слотом базового класса, недоступна. Это приводит к неоднозначному поведению программы.

  • Атрибут slots не работает для классов, наследованных, от встроенных типов переменной длины, таких как intbytes и tuple.

  • Атрибуту slots может быть назначен любой нестроковый итерируемый объект. Могут использоваться словари, значениям, соответствующим каждому ключу, может быть присвоено особое значение.

  • Назначение class работает, если оба класса имеют одинаковые slots.

  • Может использоваться множественное наследование с несколькими родительскими классами с разделением на слоты, но только одному родительскому элементу разрешено иметь атрибуты, созданные с помощью слотов (другие классы должны иметь макеты пустых слотов), нарушение вызовет исключение TypeError.


Надеюсь всё было просто и понятно, и теперь вы чаще станете использовать slots у себя в проектах.

Жду вашего мнения на эту тему, всем удачи!

Мой GitHub

Комментарии (9)


  1. maxp
    03.09.2022 17:41
    +1

    В примерах явно потерялись двойные подчеркивания.


    1. Infinitive1 Автор
      03.09.2022 17:46
      +2

      Да, подчёркивания решили не приходить на вечеринку. Но я уже поправил, должны отображаться


  1. ancheefa
    03.09.2022 17:46

    а что это за версия питона? Ато у меня AttributeError: 'NoSlots' object has no attribute 'dict'


    1. Infinitive1 Автор
      03.09.2022 17:47

      Подчёркивания не добавились. Поправил, обновите страницу


  1. Andrey_Solomatin
    03.09.2022 18:31
    +2

    Очень неплохое видео про оптимизации производительности Питона, про слоты там тоже есть.

    https://www.youtube.com/watch?v=Ix04KpZiUA8


  1. lebedec
    03.09.2022 18:48
    +1

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

    В современном Python 3 это неправда уже лет десять, с момента как стали шарить ключи словарей PEP-412

    Сегодня использование слотов не имеет особого практического смысла. Даже если у вас получится выиграть пару лишних мегабайт памяти или миллисекунд на обращение к атрибутам, по отношению к полезной нагрузке от тысяч объектов это будет незначительно.
     
    Если у вас полезной нагрузки нет, реально число-дробилка на десятки тысяч объектов и важна производительность, попробуйте “data-driven" подход. Более оптимальные структуры вроде кортежей или условных массивов numpy дадут больше профита, чем "оптимизация" объектов.


    1. kai3341
      04.09.2022 00:00
      +1

      Прикол в том, что __slots__ становятся очень интересны при их использовании не по назначению. Мне особенно доставляет их использование в DTO -- это позволяет компактнее сериализировать объекты (см. msgpack)


  1. BubaVV
    04.09.2022 19:22
    +1

    А как слоты пересекаются с датаклассами?


    1. kai3341
      05.09.2022 03:23
      +2

      Пересекаются. Плохо только, что до этого сразу не додумались, а только в 3.10 -- ИМХО очевиднейшая вещь