Использование 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
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 "
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