Главная / Курсы / Python / Слоты
# Глава 35. Слоты Концепция слотов решает сразу три задачи, регулярно встающих при работе с объектами: экономия памяти, ускорение доступа к атрибутам, запрет на добавление в инстансы классов новых атрибутов. Разберемся, каким образом этого добиться с помощью слотов. ## Мотивация Атрибуты объектов класса хранятся в dunder-поле `__dict__` типа `dict`. Мы [разбирали это](/courses/python/chapters/python_chapter_0180#block-dict) в главе про модель данных в питоне. Посмотрим, как выглядит содержимое `__dict__` объекта класса `D`: ```python {.example_for_playground} class D: def __init__(self): self.a = 1 self.b = 2 def f(self): ... print("Instance attributes", D().__dict__) ``` ``` Instance attributes {'a': 1, 'b': 2} ``` В данном случае инстанс класса содержит словарь с именами и значениями атрибутов `a` и `b`. Но этот словарь оптимизирован особым образом: начиная с версии питона 3.3 словари, хранящие атрибуты объектов, разделяют ключи между различными инстансами класса. Это улучшение было предложено в [PEP-412:](https://peps.python.org/pep-0412/) в случае, если объект типа `dict` используется для хранения атрибутов объекта, нужно ключи и хэши держать отдельно от значений. Несмотря на эту оптимизацию, поле `__dict__` остается довольно затратным с точки зрения памяти: возможность в любой момент добавлять в объект класса новые атрибуты не дается бесплатно. Однако на лету добавлять и удалять атрибуты объектов требуется довольно редко. В большинстве случаев у инстансов одного класса набор полей совпадает. А значит, напрашивается экономия: отказ от поля `__dict__`. Для этого и используются слоты. С их помощью выделяется память под фиксированное количество атрибутов. ## Что такое слоты [Слоты](https://docs.python.org/3/reference/datamodel.html#slots) (slots) — это dunder-поле класса, которому присваивается перечисление атрибутов, которыми должен обладать объект. После добавления в класс слотов его инстансам уже не получится добавлять новые атрибуты. Убедимся в этом. Добавим слоты в класс `S`: ```python {.example_for_playground} class S: __slots__ = ("a", "b") def __init__(self): self.a = 1 self.b = 2 def f(self): ... print("Slots", S().__slots__) print("Instance attributes", S().__dict__) ``` ``` Slots ('a', 'b') Traceback (most recent call last): File "example.py", line 12, in <module> print("Instance attributes", S().__dict__) ^^^^^^^^^^^^ AttributeError: 'S' object has no attribute '__dict__' ``` Как видите, добавление в класс `__slots__` предотвращает появление у объектов поля `__dict__`. Попытка присвоить значение несуществующему полю объекта тоже обречена на провал: ```python s = S() s.c = 3 ``` ``` AttributeError: 'S' object has no attribute 'c' ``` В качестве присваиваемого полю `__slots__` перечисления может выступать кортеж, список, ключи словаря. Примеры: ```python __slots__ = ["a", "b"] __slots__ = {"a": "val1", "b": "val2"} ``` ## Преимущества классов со слотами Добавление слотов преследует три цели: - Сэкономить память. Вместо словаря `__dict__` значения полей хранятся в массиве фиксированного размера. - Ускорить обращения к полям. Работа с полем превращается в доступ к элементу массива вместо обращения к хэш-таблице. - Лишить пользователя класса возможности добавлять поля в инстансы класса. Это предотвращает целый класс ошибок, связанных с опечатками в именах полей. При попытке обращения к несуществующему полю генерируется исключение `AttributeError`. Проверим, насколько класс со слотами экономнее, чем без слотов. Есть класс `Message` и список `messages`, состоящий из его инстансов. Заведите класс `MessageSlots` с таким же набором полей, но с добавлением слотов. Аналогичным образом заполните список `messages_slots`. {.task_text} С помощью функции `asizeof()` из стороннего модуля `pympler.asizeof` выведите в консоль две строки: сколько памяти занимает список `messages` и сколько занимает `messages_slots`. {.task_text} ```python {.task_source #python_chapter_0350_task_0010} class Message: def __init__(self, msg_id, text): self._id = msg_id self._text = text messages = [Message(x, str(x)) for x in range(1000)] ``` Так выглядит вывод в консоль размера `messages_slots`: `pympler.asizeof.asizeof(messages_slots)`. {.task_hint} ```python {.task_answer} import pympler.asizeof class Message: def __init__(self, msg_id, text): self._id = msg_id self._text = text class MessageSlots: __slots__ = ("_id", "_text") def __init__(self, msg_id, text): self._id = msg_id self._text = text messages = [Message(x, str(x)) for x in range(1000)] messages_slots = [MessageSlots(x, str(x)) for x in range(1000)] print(pympler.asizeof.asizeof(messages)) print(pympler.asizeof.asizeof(messages_slots)) ``` Если вам интересно, почему в данной задаче мы использовали функцию из [стороннего модуля](https://pympler.readthedocs.io/en/latest/) `pympler.asizeof.asizeof()` вместо родной функции `sys.getsizeof()`, то ответ прост: `sys.getsizeof()` считает размер объекта без учета вложенных объектов (shallow size). А `pympler.asizeof.asizeof()` считает фактический размер, занимаемый в памяти с учетом объектов, на которые ссылается интересующий объект. На самом деле то, какой объем памяти получится сэкономить с помощью слотов в конкретном участке кода, зависит от многих факторов, среди которых: - Архитектура целевой платформы. - Версия языка. - Количество полей в объекте. - Количество аллоцируемых объектов. В некоторых случаях удается добиться экономии памяти в разы. На официальном сайте docs.python.org [приведены](https://docs.python.org/3/howto/descriptor.html) следующие цифры: на 64-битной машине с Linux на борту инстанс с двумя атрибутами занял 48 байт со слотами и 152 байта без слотов. А теперь давайте посмотрим, насколько доступ к полям класса со слотами быстрее, чем к полям класса без слотов. Создайте класс `S`, такой же как `C`, но со слотом для поля `field`. {.task_text} Напишите функцию `field_rw()`, которая принимает на вход объект, записывает в его поле `field` строку `"test"` и читает это поле в некоторую переменную. {.task_text} Выведите в консоль среднее арифметическое измерений времени выполнения, полученных с помощью [функции](https://docs.python.org/3/library/timeit.html) `repeat()` из модуля `timeit`. {.task_text} ```python {.task_source #python_chapter_0350_task_0020} import timeit class C: def __init__(self, val=None): self.field = val # Implement class S # Implement function field_rw() def test_class_with_dict(): obj = C() field_rw(obj) def test_class_with_slots(): obj = S() field_rw(obj) results_dict = timeit.repeat(test_class_with_dict) results_slots = timeit.repeat(test_class_with_slots) # Print average of results_dict, results_slots ``` Пример подсчета времени выполнения `test_class_with_slots`: `timeit.repeat(test_class_with_slots)`. {.task_hint} ```python {.task_answer} import timeit class C: def __init__(self, val=None): self.field = val class S: __slots__ = ("field",) def __init__(self, val=None): self.field = val def field_rw(obj): obj.field = "test" x = obj.field def test_class_with_dict(): obj = C() field_rw(obj) def test_class_with_slots(): obj = S() field_rw(obj) results_dict = timeit.repeat(test_class_with_dict) results_slots = timeit.repeat(test_class_with_slots) print(sum(results_dict) / len(results_dict)) print(sum(results_slots) / len(results_slots)) ``` То, насколько добавление в класс слотов ускорит доступ к атрибутам, также сильно зависит от целевой платформы, версии языка и других факторов. На docs.python.org [приведен](https://docs.python.org/3/howto/descriptor.html) замер, выполненный на процессоре Apple M1 (версия питона 3.10): чтение атрибутов объектов со слотами ускорилось на 35% относительно объектов без слотов. Что выведет этот код? {.task_text} В случае не обработанного исключения напишите `error`. {.task_text} ```python {.example_for_playground} class X: __slots__ = () def __init__(self, val): self.val = val print(X("val").val) ``` ```consoleoutput {.task_source #python_chapter_0350_task_0030} ``` Поле `__slots__` класса `X` пустое. Следовательно, у объектов класса не может быть никаких атрибутов. При попытке присвоения `self.val = val` сгенерируется исключение `AttributeError: 'X' object has no attribute 'val'`. {.task_hint} ```python {.task_answer} error ``` ## Ограничения классов со слотами Добавление в класс слотов приводит к некоторым **сложностям** при [наследовании,](/courses/python/chapters/python_chapter_0170/) при [множественном наследовании](/courses/python/chapters/python_chapter_0170#block-multiple-inheritance) и при заведении [слабых ссылок](https://docs.python.org/3/library/weakref.html) (weak references) на объекты класса. **Слоты и наследование.** Значение `__slots__` наследуется. Однако если в дочернем классе появляются поля, которых нет у родителя, это приводит к появлению в дочернем классе поля `__dict__`. Чтобы этого избежать, в дочерний класс тоже нужно добавить `__slots__` и перечислить в нем новые поля. Поля класса-родителя дублировать не надо. **Слоты и множественное наследование.** Не получится наследоваться от нескольких родителей, если более чем у одного из них определены слоты. Эту проблему можно обойти с помощью [абстрактных классов.](/courses/python/chapters/python_chapter_0170#block-abstract-classes) **Слоты и создание слабых ссылок на инстансы класса.** Слабая ссылка — это ссылка, существование которой никак не помешает сборщику мусора уничтожить объект, на который она указывает. Если в класс добавлены слоты, то у его инстансов отсутствует не только `__dict__`, но и поле `__weakref__`, необходимое для заведения слабых ссылок. Исправляется это добавлением `__weakref__` в перечисление слотов. **Слоты и встроенные типы.** Не пустой атрибут `__slots__` не может быть добавлен к классу, наследованному от встроенных типов переменной длины: `int`, `bytes` и `tuple`. При попытке добавления слотов будет сгенерировано исключение `TypeError`. Данное ограничение связано со спецификой внутреннего устройства типов переменной длины. Если смотреть на то, как преобразованный в сишную структуру объект расположен в памяти, то слоты занимают место по некоему фиксированному смещению в структуре. Объекты типов переменной длины могут содержать префикс фиксированной длины перед блоком данных изменяющегося размера. Но при наследовании от этих типов нельзя выделить блок фиксированного размера, в который могли бы быть встроены слоты. Первоочередной мотивацией для реализации слотов всегда выступала скорость, поэтому было принято решение запретить слоты в сценариях, когда они не принесут желаемого ускорения. В связи с перечисленными нюансами не стоит без необходимости добавлять слоты во все подряд классы. Прибегайте к их помощи только ради существенной экономии памяти или ускорения кода. Например, если становится очевидно, что объекты какого-то класса стали бутылочным горлышком по производительности или потреблению памяти. Иначе потенциальные неудобства грозят перевесить незначительный выигрыш в ресурсах. ## Как устроены слоты После добавления в класс слотов значения атрибутов больше не привязаны к ключам в словаре `__dict__`. Теперь они содержатся в массиве фиксированной длины. Каждый атрибут, перечисленный в поле `__slots__`, — на самом деле [дескриптор,](/courses/python/chapters/python_chapter_0340/) который задает правила для чтения и изменения атрибута по индексу в массиве. Под капотом логика работы со слотами имплементирована на C. Благодаря этому доступ к слотам очень эффективен. ## Резюмируем - Слоты (slots) — это dunder-поле `__slots__` класса, которому присваивается перечисление атрибутов объекта. Если в класс добавлены слоты, то в объекты класса становится невозможно добавить новые поля кроме тех, что перечислены в слотах. - Если в классе присутствует поле `__slots__`, то в нем уже нет поля `__dict__`. - Объект со слотами занимает меньше памяти, чем объект с `__dict__`. Доступ к его атрибутам также происходит быстрее.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!