Когда мы создаем объект класса, атрибуты этого объекта сохраняются в словарь под названием __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, но когда речь заходит о создании тысяч или миллионов объектов, мы можем столкнуться со следующими проблемами:
Из документации 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. Когда мы пишем article.writer, Python использует метод __getattribute__(), который заглядывает в __dict__, обращается по ключу self.__dict__["writer"] и возвращает значение.
Если в найденном объекте определён один из методов дескриптора, то стандартное поведение будет заменено на этот метод.
Методы протокола дескриптора: __get__(), __set__() и __delete__(). Дескриптор – это просто объект Python, в котором определён хотя бы один из этих методов.
А __slots__ автоматически создаёт дескриптор для каждого атрибута с определением этих методов. Их вы увидите на скриншоте. Это значит, что для взаимодействия с атрибутами объект будет использовать методы __get__(), __set__() и __delete__(), а не стандартное поведение.
Согласно Гвидо ван Россуму, определяя __get__() и __set__(), вместо словаря мы используем массив, полностью реализованный на С, что приводит к большой эффективности кода.
В следующем коде мы сравним время создания объекта и обращения к атрибутам для классов 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) обычно справедливо только для наихудшего случая, чаще всего эта разница будет незаметна, особенно при работе с небольшими объемами данных.
В силу того, что к атрибутам можно обращаться как к элементам данных, нет необходимости хранить их в словаре __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__ – вещь замечательная. Можно ли теперь добавить её в каждый класс?
Ответ: НЕТ! Очевидно, надо стремиться к какому-то компромиссу.
Одна из причин использовать __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.
Если вы ищете работу на Python, на Talanto.work сейчас есть варианты для junior, middle, senior…
Рынок Python-вакансий большой, но удобным его назвать сложно. Формально вакансий много, однако в реальном поиске…
Срезы в Python — это больше, чем a[2:7]. В статье — быстрый разбор полезных приёмов:…
В этой статье мы рассмотрим возведение в степень в Python с помощью различных методов, от…
Дубликаты в списке Python — частая проблема. В статье показано, как быстро убрать повторы, в…
Услуга виртуального хостинга на SpaceWeb позволяет арендовать часть сервера (shared-сервера) и разместить на нём свой…