Использование slots | Python

6adf44397b1a3178690ee3dce6b9f577.jpg

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

Эта статья вдохновлена моим обучением. Когда я только начинал свой 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 
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 "", line 1, in
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 не работает для классов, наследованных, от встроенных типов переменной длины, таких как int,  bytes и tuple.

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

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

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

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

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

Мой GitHub

© Habrahabr.ru