# Глава 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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!