HTML Diff
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 &gt;&gt;&gt; obj = RegularClass() &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.foo # 5 &gt;&gt;&gt; obj.another_attribute = 'Elvis has left the building' &gt;&gt;&gt; obj.another_attribute # 'Elvis has left the building'<p>Разумеется, настолько прямолинейно это почти не используется - чаще присвоение новых атрибутов происходит внутри методов, но там работает точно такой же механизм. И это очень удобно и даёт большую гибкость. Но у этого, разумеется, есть и своя цена. А платим мы за это удобство понижением скорости доступа к атрибутам и дополнительным расходом памяти.</p>
3 class RegularClass: pass &gt;&gt;&gt; obj = RegularClass() &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.foo # 5 &gt;&gt;&gt; obj.another_attribute = 'Elvis has left the building' &gt;&gt;&gt; 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') &gt;&gt;&gt; obj = SlotsClass() &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.foo # 5 &gt;&gt;&gt; obj.another_attribute = 'Elvis has left the building' Traceback (most recent call last): File "python", line 5, in &lt;module&gt; AttributeError: 'SlotsClass' object has no attribute 'another_attribute'<p>То есть теперь мы не можем добавлять в экземпляры случайные атрибуты. Давайте разберёмся, как это влияет на производительность. Напишем небольшой тест:</p>
6 class SlotsClass: __slots__ = ('foo', 'bar') &gt;&gt;&gt; obj = SlotsClass() &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.foo # 5 &gt;&gt;&gt; obj.another_attribute = 'Elvis has left the building' Traceback (most recent call last): File "python", line 5, in &lt;module&gt; 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 &gt;&gt;&gt; import timeit &gt;&gt;&gt; min(timeit.repeat(test_foo)) 0.2567792439949699 &gt;&gt;&gt; min(timeit.repeat(test_bar)) 0.34515008199377917<p>Таким образом, получается, что класс с использованием __slots__ примерно на 25-30 % быстрее на операциях доступа к атрибутам. Замечу, что в разных версиях пайтона и разных ОС, величина расхождения может отличаться.</p>
8 &gt;&gt;&gt; import timeit &gt;&gt;&gt; min(timeit.repeat(test_foo)) 0.2567792439949699 &gt;&gt;&gt; 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 &gt;&gt;&gt; obj = RegularClass() &gt;&gt;&gt; obj.__dict__ # {} &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.__dict__ # {'foo': 5}<p>Но вот если явно указать значение__slots__, то словарь __dict__ создаваться не будет:</p>
11 class RegularClass: pass &gt;&gt;&gt; obj = RegularClass() &gt;&gt;&gt; obj.__dict__ # {} &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.__dict__ # {'foo': 5}<p>Но вот если явно указать значение__slots__, то словарь __dict__ создаваться не будет:</p>
12 class SlotsClass: __slots__ = ('foo', 'bar') &gt;&gt;&gt; obj = SlotsClass() &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.__slots__ # ('foo', 'bar') &gt;&gt;&gt; obj.__dict__ Traceback (most recent call last): File "python", line 8, in &lt;module&gt; AttributeError: 'SlotsClass' object has no attribute '__dict__'<p>Именно за счёт этого экономится память:</p>
12 class SlotsClass: __slots__ = ('foo', 'bar') &gt;&gt;&gt; obj = SlotsClass() &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.__slots__ # ('foo', 'bar') &gt;&gt;&gt; obj.__dict__ Traceback (most recent call last): File "python", line 8, in &lt;module&gt; 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 &gt;&gt;&gt; obj = ChildSlotsClass() &gt;&gt;&gt; obj.__slots__ # ('foo', 'bar') &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.something_new = 3 &gt;&gt;&gt; obj.__dict__ # {'something_new': 3}<p>Если нам нужно, чтобы и дочерний класс тоже был ограничен слотами, там придётся и в нём присвоить значение атрибуту __slots__. Кстати, дублировать уже указанные в родительском классе слоты не нужно.</p>
19 class SlotsClass: __slots__ = ('foo', 'bar') class ChildSlotsClass(SlotsClass): pass &gt;&gt;&gt; obj = ChildSlotsClass() &gt;&gt;&gt; obj.__slots__ # ('foo', 'bar') &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.something_new = 3 &gt;&gt;&gt; obj.__dict__ # {'something_new': 3}<p>Если нам нужно, чтобы и дочерний класс тоже был ограничен слотами, там придётся и в нём присвоить значение атрибуту __slots__. Кстати, дублировать уже указанные в родительском классе слоты не нужно.</p>
20 class SlotsClass: __slots__ = ('foo', 'bar') class ChildSlotsClass(SlotsClass): __slots__ = ('baz',) &gt;&gt;&gt; obj = ChildSlotsClass() &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.baz = 6 &gt;&gt;&gt; obj.something_new = 3 Traceback (most recent call last): File "python", line 12, in &lt;module&gt; AttributeError: 'ChildSlotsClass' object has no attribute 'something_new'<p>Хуже обстоит дело с<strong>множественным наследованием</strong>. Если у нас есть два родительских класса, у каждого их которых определены слоты, то попытка создать дочерний класс, увы, обречена.</p>
20 class SlotsClass: __slots__ = ('foo', 'bar') class ChildSlotsClass(SlotsClass): __slots__ = ('baz',) &gt;&gt;&gt; obj = ChildSlotsClass() &gt;&gt;&gt; obj.foo = 5 &gt;&gt;&gt; obj.baz = 6 &gt;&gt;&gt; obj.something_new = 3 Traceback (most recent call last): File "python", line 12, in &lt;module&gt; AttributeError: 'ChildSlotsClass' object has no attribute 'something_new'<p>Хуже обстоит дело с<strong>множественным наследованием</strong>. Если у нас есть два родительских класса, у каждого их которых определены слоты, то попытка создать дочерний класс, увы, обречена.</p>
21 class BaseA(object): __slots__ = ('a',) class BaseB(object): __slots__ = ('b',) &gt;&gt;&gt; class Child(BaseA, BaseB): __slots__ = () Traceback (most recent call last): File "&lt;pyshell#68&gt;", line 1, in &lt;module&gt; 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',) &gt;&gt;&gt; class Child(BaseA, BaseB): __slots__ = () Traceback (most recent call last): File "&lt;pyshell#68&gt;", line 1, in &lt;module&gt; 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