0 added
0 removed
Original
2026-01-01
Modified
2026-03-10
1
<p>Пайтон, как и другие динамические языки, славен тем, что с объектами и экземплярами в рантайме можно творить практически что угодно - добавлять атрибуты, удалять их.</p>
1
<p>Пайтон, как и другие динамические языки, славен тем, что с объектами и экземплярами в рантайме можно творить практически что угодно - добавлять атрибуты, удалять их.</p>
2
<p>Например:</p>
2
<p>Например:</p>
3
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'<p>Разумеется, настолько прямолинейно это почти не используется - чаще присвоение новых атрибутов происходит внутри методов, но там работает точно такой же механизм. И это очень удобно и даёт большую гибкость. Но у этого, разумеется, есть и своя цена. А платим мы за это удобство понижением скорости доступа к атрибутам и дополнительным расходом памяти.</p>
3
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'<p>Разумеется, настолько прямолинейно это почти не используется - чаще присвоение новых атрибутов происходит внутри методов, но там работает точно такой же механизм. И это очень удобно и даёт большую гибкость. Но у этого, разумеется, есть и своя цена. А платим мы за это удобство понижением скорости доступа к атрибутам и дополнительным расходом памяти.</p>
4
<p>При этом динамическое управление атрибутами нам нужно далеко не всегда - очень много случаев, когда мы точно знаем, какие атрибуты будут у экземпляров класса. Можем ли мы как-то уменьшить расход ресурсов в этом случае? К счастью - да.</p>
4
<p>При этом динамическое управление атрибутами нам нужно далеко не всегда - очень много случаев, когда мы точно знаем, какие атрибуты будут у экземпляров класса. Можем ли мы как-то уменьшить расход ресурсов в этом случае? К счастью - да.</p>
5
<p>Как раз для таких случаев в пайтоне есть магический атрибут __slots__, который позволяет задать ограниченный набор атрибутов, которыми будет обладать экземпляр класса.</p>
5
<p>Как раз для таких случаев в пайтоне есть магический атрибут __slots__, который позволяет задать ограниченный набор атрибутов, которыми будет обладать экземпляр класса.</p>
6
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'<p>То есть теперь мы не можем добавлять в экземпляры случайные атрибуты. Давайте разберёмся, как это влияет на производительность. Напишем небольшой тест:</p>
6
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'<p>То есть теперь мы не можем добавлять в экземпляры случайные атрибуты. Давайте разберёмся, как это влияет на производительность. Напишем небольшой тест:</p>
7
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())<p>И с помощью модуля<strong>timeit</strong>оценим время выполнения:</p>
7
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())<p>И с помощью модуля<strong>timeit</strong>оценим время выполнения:</p>
8
>>> import timeit >>> min(timeit.repeat(test_foo)) 0.2567792439949699 >>> min(timeit.repeat(test_bar)) 0.34515008199377917<p>Таким образом, получается, что класс с использованием __slots__ примерно на 25-30 % быстрее на операциях доступа к атрибутам. Замечу, что в разных версиях пайтона и разных ОС, величина расхождения может отличаться.</p>
8
>>> import timeit >>> min(timeit.repeat(test_foo)) 0.2567792439949699 >>> min(timeit.repeat(test_bar)) 0.34515008199377917<p>Таким образом, получается, что класс с использованием __slots__ примерно на 25-30 % быстрее на операциях доступа к атрибутам. Замечу, что в разных версиях пайтона и разных ОС, величина расхождения может отличаться.</p>
9
<h2>А что с памятью?</h2>
9
<h2>А что с памятью?</h2>
10
<p>Прежде всего, надо представлять, как хранятся атрибуты. У каждого экземпляра класса есть магический атрибут __dict__. Это словарь, в котором хранятся атрибуты, присвоенные экземпляру класса, и он есть у всех экземпляров класса.</p>
10
<p>Прежде всего, надо представлять, как хранятся атрибуты. У каждого экземпляра класса есть магический атрибут __dict__. Это словарь, в котором хранятся атрибуты, присвоенные экземпляру класса, и он есть у всех экземпляров класса.</p>
11
class RegularClass: pass >>> obj = RegularClass() >>> obj.__dict__ # {} >>> obj.foo = 5 >>> obj.__dict__ # {'foo': 5}<p>Но вот если явно указать значение__slots__, то словарь __dict__ создаваться не будет:</p>
11
class RegularClass: pass >>> obj = RegularClass() >>> obj.__dict__ # {} >>> obj.foo = 5 >>> obj.__dict__ # {'foo': 5}<p>Но вот если явно указать значение__slots__, то словарь __dict__ создаваться не будет:</p>
12
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__'<p>Именно за счёт этого экономится память:</p>
12
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__'<p>Именно за счёт этого экономится память:</p>
13
<ul><li><strong>attrs</strong>- количество атрибутов;</li>
13
<ul><li><strong>attrs</strong>- количество атрибутов;</li>
14
<li><strong><strong>slots</strong></strong>- размер объекта (байт) с объявленным<strong>slots</strong>;</li>
14
<li><strong><strong>slots</strong></strong>- размер объекта (байт) с объявленным<strong>slots</strong>;</li>
15
<li><strong><strong>dict</strong></strong>- размер объекта (байт) без объявленного<strong>slots</strong>;</li>
15
<li><strong><strong>dict</strong></strong>- размер объекта (байт) без объявленного<strong>slots</strong>;</li>
16
</ul><p>(Источник: https://stackoverflow.com/questions/472000/usage-of-slots).</p>
16
</ul><p>(Источник: https://stackoverflow.com/questions/472000/usage-of-slots).</p>
17
<p>Как мы видим, использовать слоты довольно просто, но есть и некоторые подводные камни. Например,<strong>наследование</strong>. Нужно помнить, что значение __slots__ наследуется, однако это не предотвращает создание __dict__.</p>
17
<p>Как мы видим, использовать слоты довольно просто, но есть и некоторые подводные камни. Например,<strong>наследование</strong>. Нужно помнить, что значение __slots__ наследуется, однако это не предотвращает создание __dict__.</p>
18
<p>Таким образом, дочерние классы будут позволять добавлять динамические атрибуты, и добавляться они будут в__dict__, со всеми вытекающими расходами.</p>
18
<p>Таким образом, дочерние классы будут позволять добавлять динамические атрибуты, и добавляться они будут в__dict__, со всеми вытекающими расходами.</p>
19
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}<p>Если нам нужно, чтобы и дочерний класс тоже был ограничен слотами, там придётся и в нём присвоить значение атрибуту __slots__. Кстати, дублировать уже указанные в родительском классе слоты не нужно.</p>
19
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}<p>Если нам нужно, чтобы и дочерний класс тоже был ограничен слотами, там придётся и в нём присвоить значение атрибуту __slots__. Кстати, дублировать уже указанные в родительском классе слоты не нужно.</p>
20
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'<p>Хуже обстоит дело с<strong>множественным наследованием</strong>. Если у нас есть два родительских класса, у каждого их которых определены слоты, то попытка создать дочерний класс, увы, обречена.</p>
20
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'<p>Хуже обстоит дело с<strong>множественным наследованием</strong>. Если у нас есть два родительских класса, у каждого их которых определены слоты, то попытка создать дочерний класс, увы, обречена.</p>
21
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<p>Один из способов решения этой проблемы -<strong>абстрактные классы</strong>. Но о них мы поговорим в следующий раз.</p>
21
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<p>Один из способов решения этой проблемы -<strong>абстрактные классы</strong>. Но о них мы поговорим в следующий раз.</p>
22
<p>Ещё одна небольшая проблема - отсутствие поддержки слабых ссылок на экземпляры классов со слотами. Упирается она в то, что при объявлении слотов у экземпляров перестаёт создаваться не только __dict__, но и атрибут __weakref__, необходимый для этой самой поддержки. Но решается это просто - нам нужно просто добавить __weakref__ в список слотов.</p>
22
<p>Ещё одна небольшая проблема - отсутствие поддержки слабых ссылок на экземпляры классов со слотами. Упирается она в то, что при объявлении слотов у экземпляров перестаёт создаваться не только __dict__, но и атрибут __weakref__, необходимый для этой самой поддержки. Но решается это просто - нам нужно просто добавить __weakref__ в список слотов.</p>
23
<p><em>Остались вопросы? Пишите комментарии!</em></p>
23
<p><em>Остались вопросы? Пишите комментарии!</em></p>
24
24