Python slots мы dict

__slots__ в Python

Когда мы создаем объект класса, атрибуты этого объекта сохраняются в словарь под названием __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__) # print(Article.__dict__) # , # '__dict__': , '__weakref__': # , # '__doc__': None> article.reviewer = "jojo" print(article.__dict__) # print(article.reviewer) # jojo

Это хорошо?

Ну, мы не можем сказать, что это плохо, пока не найдём решение получше. Словарь – очень мощный инструмент Python, но когда речь заходит о создании тысяч или миллионов объектов, мы можем столкнуться со следующими проблемами:

  1. Словарю нужна память. Миллионы объектов определённо съедят много оперативной памяти.
  2. Словарь по сути является хэш-таблицей. В наихудшем случае сложность операций get/set в хэш-таблице составляет O(n).

Решение с помощью __slots__

Из документации Python: __slots__ позволяет явно объявлять элементы данных (например, свойства), не прибегая к созданию __dict__ и __weakref__ (за исключением тех случаев, когда они объявлены в __slots__ явно или доступны в родительском классе).

Читайте также:  Таблица html цветов бордовый

Как это относится к вышеописанным проблемам?

Создадим класс ArticleWithSlots . Единственное различие между нашими двумя классами – дополнительное поле __slots__ .

class ArticleWithSlots: __slots__ = ["date", "writer"] def __init__(self, date, writer): self.date = date self.writer = writer

__slots__ создаётся на уровне класса, а это значит, что если мы выведем ArticleWithSlots.__dict__ , мы должны его увидеть. Помимо того, мы видим ещё 2 атрибута: date: и writer: – они принадлежат классу member_descriptor.

print(ArticleWithSlots.__dict__) # , # ‘date’: , # ‘writer’: , # ‘__doc__’: None> print(ArticleWithSlots.date.__class__) #

Что такое дескриптор в 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 = b = > 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__) #

Наследование

Если вам нужно унаследовать класс с атрибутом __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» # class ArticleAdvancedWithoutSlots(ArticleBase): pass article = ArticleAdvancedWithoutSlots() article.reviewer = «jojo» print(article.__dict__) #

Если наследоваться от класса без __slots__ , подкласс будет содержать __dict__ .

class Article: pass class ArticleWithSlots(Article): __slots__ = [«date», «writer»] article = ArticleWithSlots() article.writer = «xiaoxu» article.reviewer = «jojo» print(article.__dict__) #

Заключение

Надеюсь, что вам теперь понятно, что такое __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

Надеюсь, статья вам понравилась!

Источник

Оцените статью