Слоты в Python

webdev_Deep_9.4_site-5020-b2c633.png

Пайтон, как и другие динамические языки, славен тем, что с объектами и экземплярами в рантайме можно творить практически что угодно — добавлять атрибуты, удалять их. Например:

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'

Разумеется, настолько прямолинейно это почти не используется — чаще присвоение новых атрибутов происходит внутри методов, но там работает точно такой же механизм. И это очень удобно и даёт большую гибкость. Но у этого, разумеется, есть и своя цена. А платим мы за это удобство понижением скорости доступа к атрибутам и дополнительным расходом памяти.

При этом динамическое управление атрибутами нам нужно далеко не всегда — очень много случаев, когда мы точно знаем, какие атрибуты будут у экземпляров класса. Можем ли мы как-то уменьшить расход ресурсов в этом случае? К счастью — да.

Как раз для таких случаев в пайтоне есть магический атрибут __slots__, который позволяет задать ограниченный набор атрибутов, которыми будет обладать экземпляр класса.

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

Таким образом, получается, что класс с использованием __slots__ примерно на 25-30 % быстрее на операциях доступа к атрибутам. Замечу, что в разных версиях пайтона и разных ОС, величина расхождения может отличаться.

А что с памятью?

Прежде всего, надо представлять, как хранятся атрибуты. У каждого экземпляра класса есть магический атрибут __dict__. Это словарь, в котором хранятся атрибуты, присвоенные экземпляру класса, и он есть у всех экземпляров класса.

class RegularClass:
    pass

>>> obj = RegularClass()
>>> obj.__dict__
# {}
>>> obj.foo = 5
>>> obj.__dict__
# {'foo': 5} 

Но вот если явно указать значение__slots__, то словарь __dict__ создаваться не будет:

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__'

Именно за счёт этого экономится память:

1-20219-7e4366.jpg

  • attrs — количество атрибутов;
  • slots — размер объекта (байт) с объявленным slots;
  • dict — размер объекта (байт) без объявленного slots;

(Источник: https://stackoverflow.com/questions/472000/usage-of-slots).

Как мы видим, использовать слоты довольно просто, но есть и некоторые подводные камни. Например, наследование. Нужно помнить, что значение __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 = 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

Один из способов решения этой проблемы — абстрактные классы. Но о них мы поговорим в следующий раз.

Ещё одна небольшая проблема — отсутствие поддержки слабых ссылок на экземпляры классов со слотами. Упирается она в то, что при объявлении слотов у экземпляров перестаёт создаваться не только __dict__, но и атрибут __weakref__, необходимый для этой самой поддержки. Но решается это просто — нам нужно просто добавить __weakref__ в список слотов.

Остались вопросы? Пишите комментарии!

Автор
0 комментариев
Для комментирования необходимо авторизоваться