Слоты в Python
Пайтон, как и другие динамические языки, славен тем, что с объектами и экземплярами в рантайме можно творить практически что угодно — добавлять атрибуты, удалять их.
Например:
class RegularClass: pass >>> obj = RegularClass() >>> obj.foo = 5 >>> obj.foo # 5 >>> obj.another_attribute = 'Elvis has left the building' >>> obj.another_attribute # 'Elvis has left the building'
Разумеется, настолько прямолинейно это почти не используется — чаще присвоение новых атрибутов происходит внутри методов, но там работает точно такой же механизм. И это очень удобно и даёт большую гибкость. Но у этого, разумеется, есть и своя цена. А платим мы за это удобство понижением скорости доступа к атрибутам и дополнительным расходом памяти.
При этом динамическое управление атрибутами нам нужно далеко не всегда — очень много случаев, когда мы точно знаем, какие атрибуты будут у экземпляров класса. Можем ли мы как-то уменьшить расход ресурсов в этом случае? К счастью — да.
Как раз для таких случаев в пайтоне есть магический атрибут
class SlotsClass: __slots__ = ('foo', 'bar') >>> obj = SlotsClass() >>> obj.foo = 5 >>> obj.foo # 5 >>> obj.another_attribute = 'Elvis has left the building' Traceback (most recent call last): File "python", line 5, in <module> AttributeError: 'SlotsClass' object has no attribute 'another_attribute'
То есть теперь мы не можем добавлять в экземпляры случайные атрибуты. Давайте разберёмся, как это влияет на производительность. Напишем небольшой тест:
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
Таким образом, получается, что класс с использованием
А что с памятью?
Прежде всего, надо представлять, как хранятся атрибуты. У каждого экземпляра класса есть магический атрибут
class RegularClass: pass >>> obj = RegularClass() >>> obj.__dict__ # {} >>> obj.foo = 5 >>> obj.__dict__ # {'foo': 5}
Но вот если явно указать значение
class SlotsClass: __slots__ = ('foo', 'bar') >>> obj = SlotsClass() >>> obj.foo = 5 >>> obj.__slots__ # ('foo', 'bar') >>> obj.__dict__ Traceback (most recent call last): File "python", line 8, in <module> AttributeError: 'SlotsClass' object has no attribute '__dict__'
Именно за счёт этого экономится память:
- attrs — количество атрибутов;
- slots — размер объекта (байт) с объявленным slots;
- dict — размер объекта (байт) без объявленного slots;
(Источник: https://stackoverflow.com/questions/472000/usage-of-slots).
Как мы видим, использовать слоты довольно просто, но есть и некоторые подводные камни. Например, наследование. Нужно помнить, что значение
Таким образом, дочерние классы будут позволять добавлять динамические атрибуты, и добавляться они будут в
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}
Если нам нужно, чтобы и дочерний класс тоже был ограничен слотами, там придётся и в нём присвоить значение атрибуту
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 <module> AttributeError: 'ChildSlotsClass' object has no attribute 'something_new'
Хуже обстоит дело с множественным наследованием. Если у нас есть два родительских класса, у каждого их которых определены слоты, то попытка создать дочерний класс, увы, обречена.
class BaseA(object): __slots__ = ('a',) class BaseB(object): __slots__ = ('b',) >>> class Child(BaseA, BaseB): __slots__ = () Traceback (most recent call last): File "<pyshell#68>", line 1, in <module> class Child(BaseA, BaseB): __slots__ = () TypeError: Error when calling the metaclass bases multiple bases have instance lay-out conflict
Один из способов решения этой проблемы — абстрактные классы. Но о них мы поговорим в следующий раз.
Ещё одна небольшая проблема — отсутствие поддержки слабых ссылок на экземпляры классов со слотами. Упирается она в то, что при объявлении слотов у экземпляров перестаёт создаваться не только
Остались вопросы? Пишите комментарии!