__slots__ в Python
Это хорошо?
Ну, мы не можем сказать, что это плохо, пока не найдём решение получше. Словарь – очень мощный инструмент 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. Нет необходимости повторно указывать все атрибуты в подклассе.
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.