Как и когда использовать __slots__ в python
Каждый объект python имеет атрибут _dict_, который представляет собой словарь, содержащий
все остальные атрибуты. Например, когда вы набираете self.attr, python на самом деле делает следующее
self.dict[‘attr’].
Как вы можете себе представить, использование словаря для хранения атрибутов занимает некоторое дополнительное место
& время для доступа к нему.
Что если вы уже знаете, какие атрибуты будут у вашего класса. Тогда
использование _dict_ для хранения атрибутов не кажется хорошей идеей, поскольку нам не нужна
нам не нужна динамическая природа dicts .
Здесь на помощь приходят слоты .
Что такое слоты в python
_slots_ — это переменная класса, которая позволяет нам объявлять атрибуты класса.
класса в явном виде и отрицает создание _dict_ и
_weakref_ для экземпляров объектов.
Ожидаемые атрибуты могут быть назначены _slots_ в виде строки, итерабельной переменной,
или последовательность строк с именами переменных, используемых экземпляром.
Экономия места за счет отсутствия _dict_ может быть значительной. Слоты также
улучшают скорость поиска атрибутов.
Использование слотов
Python использует значительно более компактное внутреннее представление для
экземпляров, когда вы определяете _слоты_. Вместо словаря каждый экземпляр
создается вокруг небольшого массива фиксированного размера, похожего на кортеж или список.
В результате использования слотов вы больше не можете добавлять новые атрибуты к
экземплярам;
вы ограничены именами атрибутов, указанными в спецификаторе _slots_.( Вы можете обойти это, добавив _dlots_.
можно обойти это, добавив _dict_ к _slot_ . Диктант будет
инициализироваться только при добавлении динамического атрибута).
Пример использования слотов:
# A email class with out using slots class Email : def __init__(self,subject,to,message) : self.subject = subject self.message = message self.to = to class EmailWithSlots : __slots__ = ('subject','to','message') def __init__(self,subject,to,message) : self.subject = subject self.message = message self.to = to email = EmailWithSlots('test','me@gmail.com','testing slots') email.subject # >> test email.__dict__ # cant access __dict__ because its not created # AttributeError Traceback (most recent call last) # in # ----> 1 email.__dict__ # #AttributeError: 'EmailWithSlots' object has no attribute '__dict__' email.from = "aabid@gmail.com" # cant add an atribute that not in __slots__ # --------------------------------------------------------------------------- # AttributeError Traceback (most recent call last) # in # ----> email.from = "aabid@gmail.com" # #AttributeError: 'EmailWithSlots' object has no attribute 'from'
Плюсы и минусы слотов
Плюсы
- использование _слотов_ ускоряет доступ к атрибутам .
- _slots_ уменьшает использование памяти
Минусы
- Фиксированные атрибуты (Вы можете обойти это, добавив _dict_ к _slots_ . Диктант будет инициализироваться только при добавлении динамического атрибута )
- _slots_ реализованы на уровне класса путем создания дескрипторов для каждого имени переменной. В результате атрибуты класса не могут быть использованы для установки значений по умолчанию для переменных экземпляра, определенных _slots_; в противном случае атрибут класса перезапишет назначение дескриптора.
Наследование со слотами .
Работа со слотами и наследованием немного сложна и требует внимания.
Вот как:
class BaseClass : __slots__ = ['x','y','z'] class Inherited(BaseClass) : pass Inherited.__slots__ #>> ['x', 'y', 'z']
- Если в подклассе не указаны слоты (пустые или новые) . Подкласс получит атрибуты _dict_ и _weakref_ в дополнение к слотам родительского класса.
class BaseClass : __slots__ = ['x','y','z'] class Inherited(BaseClass) : pass class InheritedWithSlots(BaseClass) : __slots__ = () Inherited.__slots__ #>> ['x', 'y', 'z'] Inherited.__dict__ #>> <> InheritedWithSlots().__dict__ # AttributeError
- Непустые _слоты_ не работают для классов, производных от встроенных типов «переменной длины», таких как int, bytes и tuple.
# this works fine because we are not using any additional slots in subclass class MyInt(int): __slots__ = () # This will panic because we are adding non-empty slots . class NewInt(int) : __slots__ = (x,y) #TypeError: nonempty __slots__ not supported for subtype of 'int'
#lets have three slotted base classes class foo: __slots__ = ("x","y") class bar : __slots__ = ("a","b") class baz : __slots__=() # this will raise TypeError as we are inheriting from two classes # with nonempty slots class foobar(foo,bar) : pass #>>TypeError: multiple bases have instance lay-out conflict # This shall work as only one of the inherited class has nonempty slots class FooBaz(foo,bar) : pass
Заключение :
_слоты_ позволяют нам явно указать, какие переменные экземпляра следует ожидать в
объект
Что дает нам следующие преимущества
Типичный случай использования
Для классов, которые в основном служат в качестве простых структур данных, часто можно значительно уменьшить
объем памяти экземпляров, добавив атрибут _slots_ в
определению класса.
Когда слоты — плохая идея
- Избегайте их, когда вы хотите выполнить _классовое_ присвоение с другим классом, у которого их нет (и вы не можете их добавить), если только расположение слотов не идентично. (Мне очень интересно узнать, кто и зачем это делает).
- Избегайте их, если вы хотите подклассифицировать встроенные элементы переменной длины, такие как long, tuple или str, и хотите добавить к ним атрибуты.
- Избегайте их, если вы настаиваете на предоставлении значений по умолчанию через атрибуты класса для переменных экземпляра.
Спасибо, что дочитали до конца, счастливого кодинга.
Использование 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__)
Поскольку каждый объект в 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__)
Пример 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__
Если нам нужно, чтобы и дочерний класс тоже был ограничен слотами, там придётся и в нём присвоить значение атрибуту slots. Кстати, дублировать уже указанные в родительском классе слоты не нужно.
class SlotsClass: __slots__ = ('foo', 'bar') class ChildSlotsClass(SlotsClass): __slots__ = ('baz',) >>> obj = ChildSlotsClass() >>> obj.foo = 5 >>> 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 у себя в проектах.
Жду вашего мнения на эту тему, всем удачи!