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

от автора

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

Эта статья вдохновлена моим обучением. Когда я только начинал свой 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: https://github.com/Ryize


ссылка на оригинал статьи https://habr.com/ru/post/686220/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *