Когда мы создаем объект класса, атрибуты этого объекта сохраняются в словарь под названием __dict__
. Этот словарь мы используем, когда присваиваем и считываем значения атрибутов. Это позволяет нам динамически внедрять новые атрибуты уже после создания объекта.
Давайте создадим простой класс Article
, у которого изначально есть два атрибута: date
и writer
. Если мы выведем __dict__
данного объекта, то получим ключи и значения для каждого атрибута. Также мы выведем __dict__
для самого класса – это нам понадобится позже. После этого добавим в объект новый атрибут reviewer
и увидим его в обновленном __dict__
.
class Article: def __init__(self, date, writer): self.date = date self.writer = writer article = Article("2020-06-01","xiaoxu") print(article.__dict__) # {'date': '2020-06-01', 'writer': 'xiaoxu'} print(Article.__dict__) # {'__module__': '__main__', '__init__': <function Article.__init__ at # 0x10d28f0e0>, # '__dict__': <attribute '__dict__' of 'Article' objects>, '__weakref__': # <attribute '__weakref__' of 'Article' objects>, # '__doc__': None} article.reviewer = "jojo" print(article.__dict__) # {'date': '2020-06-01', 'writer': 'xiaoxu', 'reviewer': 'jojo'} print(article.reviewer) # jojo
Это хорошо?
Ну, мы не можем сказать, что это плохо, пока не найдём решение получше. Словарь – очень мощный инструмент Python, но когда речь заходит о создании тысяч или миллионов объектов, мы можем столкнуться со следующими проблемами:
- Словарю нужна память. Миллионы объектов определённо съедят много оперативной памяти.
- Словарь по сути является хэш-таблицей. В наихудшем случае сложность операций get/set в хэш-таблице составляет O(n).
Решение с помощью __slots__
Из документации Python: __slots__
позволяет явно объявлять элементы данных (например, свойства), не прибегая к созданию __dict__
и __weakref__
(за исключением тех случаев, когда они объявлены в __slots__
явно или доступны в родительском классе).
Как это относится к вышеописанным проблемам?
Создадим класс ArticleWithSlots
. Единственное различие между нашими двумя классами – дополнительное поле __slots__
.
class ArticleWithSlots: __slots__ = ["date", "writer"] def __init__(self, date, writer): self.date = date self.writer = writer
__slots__
создаётся на уровне класса, а это значит, что если мы выведем ArticleWithSlots.__dict__
, мы должны его увидеть. Помимо того, мы видим ещё 2 атрибута: date: <member 'date' ..>
и writer: <member 'writer' ..>
– они принадлежат классу member_descriptor.
print(ArticleWithSlots.__dict__) # {'__module__': '__main__', '__slots__': ['date', 'writer'], # '__init__': <function ArticleWithSlots.__init__ at 0x103f6c290>, # 'date': <member 'date' of 'ArticleWithSlots' objects>, # 'writer': <member 'writer' of 'ArticleWithSlots' objects>, # '__doc__': None} print(ArticleWithSlots.date.__class__) # <class 'member_descriptor'>[python_ad_block]
Что такое дескриптор в Python?
Перед тем, как говорить о протоколе дескриптора, мы должны рассмотреть обычный случай обращения к атрибутам в Python. Когда мы пишем article.writer
, Python использует метод __getattribute__()
, который заглядывает в __dict__
, обращается по ключу self.__dict__["writer"]
и возвращает значение.
Если в найденном объекте определён один из методов дескриптора, то стандартное поведение будет заменено на этот метод.

Методы протокола дескриптора: __get__()
, __set__()
и __delete__()
. Дескриптор – это просто объект Python, в котором определён хотя бы один из этих методов.
А __slots__
автоматически создаёт дескриптор для каждого атрибута с определением этих методов. Их вы увидите на скриншоте. Это значит, что для взаимодействия с атрибутами объект будет использовать методы __get__()
, __set__()
и __delete__()
, а не стандартное поведение.
Согласно Гвидо ван Россуму, определяя __get__()
и __set__()
, вместо словаря мы используем массив, полностью реализованный на С, что приводит к большой эффективности кода.
__slots__ позволяет быстрее обращаться к атрибутам
В следующем коде мы сравним время создания объекта и обращения к атрибутам для классов Article
и ArticleWithSlots
. __slots__
даёт ускорение на 10%.
@Timer() def create_object(cls, size): for _ in range(size): article = cls("2020-01-01", "xiaoxu") create_object(Article, 1000000) # 0.755430193 сек. create_object(ArticleWithSlots, 1000000) # 0.6753360239999999 seconds @Timer() def access_attribute(obj, size): for _ in range(size): writer = obj.writer article = Article("2020-01-01", "xiaoxu") article_slots = ArticleWithSlots("2020-01-01", "xiaoxu") access_attribute(article, 1000000) # 0.06791842000000003 сек. access_attribute(article_slots, 1000000) # 0.06492474199999987 сек.
__slots__
обеспечивает чуть большую производительность. А всё потому, что сложность операций get и set в списке меньше, чем в словаре (если рассматривать сложность в наихудшем случае).Так как O(n) обычно справедливо только для наихудшего случая, чаще всего эта разница будет незаметна, особенно при работе с небольшими объемами данных.

__slots__ сокращает использование оперативной памяти
В силу того, что к атрибутам можно обращаться как к элементам данных, нет необходимости хранить их в словаре __dict__
. На самом деле, __slots__
вообще не допустит создания __dict__
. Так что, если вы попробуете вывести article_slots.__dict__
, получите исключение AttributeError.
article_slots = ArticleWithSlots("2020-01-01", "xiaoxu") print(article_slots.__dict__) #AttributeError: 'ArticleWithSlots' object has no attribute '__dict__'
А ещё такое поведение использует меньше оперативной памяти объектом. Сравним размеры article
и article_slots
с помощью pympler. Мы не будем использовать sys.getsizeof()
, потому что getsizeof()
не учитывает размер всего, на что ссылается наш объект. Именно поэтому __dict__
будет проигнорирован getsizeof()
.
from pympler import asizeof import sys a = {"key":"value"} b = {"key":{"key2":"value"}} print(sys.getsizeof(a)) # 248 print(sys.getsizeof(b)) # 248 print(asizeof.asizeof(a)) # 360 print(asizeof.asizeof(b)) # 664
Оказывается, article_slots
экономит нам более 50% памяти. Значительное улучшение!
from pympler import asizeof article = Article("2020-01-01", "xiaoxu") article_slots = ArticleWithSlots("2020-01-01", "xiaoxu") print(asizeof.asizeof(article)) # 416 print(asizeof.asizeof(article_slots)) # 184
Мы наблюдаем такой результат потому, что в article_slots
больше не создается атрибут __dict__
, который раньше занимал много памяти.
Когда следует использовать __slots__?
Похоже, __slots__
– вещь замечательная. Можно ли теперь добавить её в каждый класс?
Ответ: НЕТ! Очевидно, надо стремиться к какому-то компромиссу.
Фиксированные атрибуты
Одна из причин использовать __dict__
– его гибкость: после создания объекта можно добавить к нему новые атрибуты. А вот __slots__
при создании объекта зафиксирует его состав. Поэтому новые атрибуты добавить уже не получится.
article_slots = ArticleWithSlots("2020-01-01", "xiaoxu") article_slots.reviewer = "jojo" # AttributeError: 'ArticleWithSlots' object has no attribute 'reviewer'
Однако…
Иногда можно воспользоваться преимуществами __slots__
и одновременно обеспечить возможность добавления новых атрибутов. Этого можно добиться, указав __dict__
внутри __slots__
в качестве одного из атрибутов. Однако, в этом случае в __dict__
появятся только новые, добавленные атрибуты. Такой прием может пригодиться, когда в классе 10+ зафиксированных атрибутов, а вам в дальнейшем не помешают 1 или 2 динамических.
class ArticleWithSlotsAndDict: __slots__ = ["date", "writer", "__dict__"] def __init__(self, date, writer): self.date = date self.writer = writer article_slots_dict = ArticleWithSlotsAndDict("2020-01-01", "xiaoxu") print(article_slots_dict.__dict__) # {} article_slots_dict.reviewer = "jojo" print(article_slots_dict.__dict__) # {'reviewer': 'jojo'}
Наследование
Если вам нужно унаследовать класс с атрибутом __slots__
, нет нужды заново указывать эти атрибуты в подклассе. Иначе подкласс займёт больше места. Кроме того, повторяющиеся атрибуты будут недоступны в родительском классе.
class ArticleBase: __slots__ = ["date", "writer"] class ArticleAdvanced(ArticleBase): __slots__ = ["reviewer"] article = ArticleAdvanced() article.writer = "xiaoxu" article.reviewer = "jojo" print(ArticleBase.writer.__get__(article)) # xiaoxu print(ArticleAdvanced.reviewer.__get__(article)) # jojo
То же самое происходит, когда мы наследуемся от NamedTuple
. Нет необходимости повторно указывать все атрибуты в подклассе (подробнее о NamedTuple
– в другой статье автора).
import collections ArticleNamedTuple = collections.namedtuple("ArticleNamedTuple", ["date", "writer"]) class ArticleAdvancedNamedTuple(ArticleNamedTuple): __slots__ = () article = ArticleAdvancedNamedTuple("2020-01-01", "xiaoxu") print(article.writer) # xiaoxu
Атрибут __dict__
можно также добавить в подклассе. Или можно просто не указывать __slots__
в подклассе, тогда в нём по умолчанию появится __dict__
.
class ArticleBase: __slots__ = ["date", "writer"] class ArticleAdvanced(ArticleBase): __slots__ = ["__dict__"] article = ArticleAdvanced() article.reviewer = "jojo" # {'reviewer': 'jojo'} class ArticleAdvancedWithoutSlots(ArticleBase): pass article = ArticleAdvancedWithoutSlots() article.reviewer = "jojo" print(article.__dict__) # {'reviewer': 'jojo'}
Если наследоваться от класса без __slots__
, подкласс будет содержать __dict__
.
class Article: pass class ArticleWithSlots(Article): __slots__ = ["date", "writer"] article = ArticleWithSlots() article.writer = "xiaoxu" article.reviewer = "jojo" print(article.__dict__) # {'reviewer': 'jojo'}
Заключение
Надеюсь, что вам теперь понятно, что такое __slots__
и как его можно использовать. В конце статьи я приведу плюсы и минусы этого приема, основанные на моём собственном опыте и данных некоторых интернет-ресурсов.
Плюсы
Применение __slots__
точно будет оправданным, когда приходится экономить память. Его крайне легко добавить и удалить – всего лишь одна строчка кода. Благодаря возможности указать __dict__
в качестве атрибута __slots__
разработчики могут без проблем работать с атрибутами, одновременно заботясь о производительности.
Минусы
Нужно чётко осознавать, чего вы хотите добиться, используя __slots__
, особенно при наследовании класса с этим свойством – в этом случае на результат может повлиять много различных факторов.
Невозможно наследоваться от встроенных типов (таких как int
, bytes
, tuple
) с непустыми __slots__
. Кроме того, вы не можете установить значение по умолчанию для атрибутов в __slots__
. Всё потому, что эти атрибуты должны быть дескрипторами. Вместо этого можно присвоить значение по умолчанию в __init __()
.
class ArticleNumber(int): __slots__ = ["number"] # TypeError: nonempty __slots__ not supported for subtype of 'int' class Article: __slots__ = ["date", "writer"] date = "2020-01-01" # ValueError: 'date' in __slots__ conflicts with class variable
Надеюсь, статья вам понравилась!
Перевод статьи Understand slots in Python.