Для начала небольшой дисклеймер.
Эта статья вдохновлена моим обучением. Когда я только начинал свой 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
не работает для классов, наследованных, от встроенных типов переменной длины, таких какint
,bytes
иtuple
. -
Атрибуту
slots
может быть назначен любой нестроковый итерируемый объект. Могут использоваться словари, значениям, соответствующим каждому ключу, может быть присвоено особое значение. -
Назначение
class
работает, если оба класса имеют одинаковыеslots
. -
Может использоваться множественное наследование с несколькими родительскими классами с разделением на слоты, но только одному родительскому элементу разрешено иметь атрибуты, созданные с помощью слотов (другие классы должны иметь макеты пустых слотов), нарушение вызовет исключение
TypeError
.
Надеюсь всё было просто и понятно, и теперь вы чаще станете использовать slots у себя в проектах.
Жду вашего мнения на эту тему, всем удачи!
Мой GitHub: https://github.com/Ryize
Автор: Матвей