Как стать автором
Обновить
633.64
OTUS
Цифровые навыки от ведущих экспертов

slots в Python

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров3.4K

Привет, Хабр!

Сегодня мы рассмотри замечательный механизм в Python — slots. Они помогают бороться с утечками памяти и тормозами в системах, где создается миллион объектов.

Каждый экземпляр класса в Python хранит свои атрибуты в словаре dict. Это дает некую гибкость — можно динамически добавлять атрибуты, менять их на ходу. Но за такую гибкость приходится платить — расход памяти растет, а это критично, когда речь идет о сотнях тысяч или миллионах объектов.

slots позволяет заранее зафиксировать набор атрибутов для класса, тем самым исключая создание дополнительного словаря, что приводит к уменьшению объема памяти, занимаемой каждым объектом.

Внутреннее устройство slots

Когда мы объявляем атрибут slots в классе, Python не создает обычный dict, а выделяет компактную структуру, представляющую фиксированный набор «полей». Рассмотрим простой пример:

class Normal:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Slotted:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

В классе Normal каждый объект содержит словарь dict, в котором динамически размещаются ссылки на x и y. В то время как Slotted заранее знает, что его атрибуты — это только x и y, и их место в памяти выделяется статически.

Технические нюансы

Самая очевидная особенность использования slots — вы теряете возможность динамически добавлять новые атрибуты. Попробуйте сделать так:

obj = Slotted(10, 20)
try:
    obj.z = 30  # Попытка добавить новый атрибут
except AttributeError as e:
    print("Ошибка:", e)

Такой код выдаст AttributeError, потому что атрибут z не объявлен в slots. Если необходима динамичность, можно добавить специальное поле '__dict__' в slots, но тогда выгода по памяти практически теряется.

Стоит также сказать про наследование. Если базовый класс определен с slots, то дочерний класс не будет иметь dict по дефолту, если вы явно не укажете slots и для него. Пример:

class Parent:
    __slots__ = ('a',)
    def __init__(self, a):
        self.a = a

class Child(Parent):
    __slots__ = ('b',)
    def __init__(self, a, b):
        super().__init__(a)
        self.b = b

child = Child(1, 2)
try:
    child.c = 3  # Ошибка, потому что 'c' не объявлен ни в Parent, ни в Child
except AttributeError as e:
    print("Ошибка:", e)

При множественном наследовании все родительские классы должны быть слотовыми. Если хотя бы один из них не использует slots, система не сможет объединить их slots корректно.

По умолчанию слотовые классы не поддерживают слабые ссылки, так как для этого требуется поле weakref в slots. Если нужно, чтобы объекты поддерживали weakref, обязательно добавьте '__weakref__' в список slots:

class WeakSlotted:
    __slots__ = ('x', 'y', '__weakref__')
    def __init__(self, x, y):
        self.x = x
        self.y = y

import weakref

obj = WeakSlotted(100, 200)
ref = weakref.ref(obj)
print("Слабая ссылка:", ref())

Без добавления weakref вы получите ошибку, когда попытаетесь создать слабую ссылку на экземпляр.

Еще один нюанс: slots может осложнить сериализацию объектов (например, через модуль pickle). Если класс не имеет dict, стандартный механизм сериализации может не справиться с сохранением состояния. Решением может быть реализация специальных методов getstate и setstate, чтобы явно указать, что именно нужно сериализовать.

Интеграция с @dataclass

С выходом Python 3.10 и дальнейшей поддержки параметра slots=True в декораторе @dataclass, стало намного удобнее создавать слотовые классы, сохраняя адекватный синтаксис. Пример:

from dataclasses import dataclass

@dataclass(slots=True)
class Point:
    x: int
    y: int

p = Point(10, 20)
print(f"Point: ({p.x}, {p.y})")
try:
    p.z = 30  # Попытка добавить новый атрибут
except AttributeError as e:
    print("Ошибка:", e)

Объединяем все фичи dataclass (автоматическая генерация init, repr и т. д.) с преимуществами экономии памяти от slots.

Примеры из реальной жизни

ML/ETL пайплайны

Представляем себе, что мы обрабатываем огромный поток данных, где для каждого элемента создается объект с фиксированным набором атрибутов. Если таких объектов — миллион, а у каждого есть свой dict, то расход памяти может просто взорваться. С slots фиксируем набор атрибутов и резко снижаем накладные расходы. Пример:

class DataPoint:
    __slots__ = ('id', 'timestamp', 'value')
    def __init__(self, id, timestamp, value):
        self.id = id
        self.timestamp = timestamp
        self.value = value

# Генерируем данные в ETL-процессе:
data_points = [DataPoint(i, 1627845123 + i, i * 0.5) for i in range(1_000_000)]

Всего пара строчек кода — и вы уже экономите кучу памяти.

Серверные приложения

В высоконагруженных серверных приложениях каждый клиент часто представлен объектом. Чем меньше памяти у объекта, тем больше клиентов вы сможете обслужить без лишней нагрузки на систему. Пример для подключения клиента:

class ClientConnection:
    __slots__ = ('client_id', 'ip_address', 'port', 'connected')
    def __init__(self, client_id, ip_address, port):
        self.client_id = client_id
        self.ip_address = ip_address
        self.port = port
        self.connected = True

# Симулируем создание нескольких подключений:
connections = [ClientConnection(i, f"192.168.0.{i % 255}", 8000 + i % 100) for i in range(10000)]

Меньше накладных расходов — быстрее обработка запросов и стабильность сервера.

Кэширование и объектные пулы

В системах кэширования или объектных пулах постоянно создаются и уничтожаются тысячи объектов. Применяя slots, четко фиксируем набор атрибутов и облегчаем сборку мусора, т.к система знает, что не будет никаких неожиданных атрибутов. Пример простого объекта для кэширования:

class CacheItem:
    __slots__ = ('key', 'value', 'timestamp')
    def __init__(self, key, value, timestamp):
        self.key = key
        self.value = value
        self.timestamp = timestamp

# Представим, что это наш кэш с объектным пулом:
cache = {f"item_{i}": CacheItem(f"item_{i}", i * 10, 1627845123 + i) for i in range(5000)}

Если у вас остались вопросы или вы хотите поделиться своим опытом — пишите в комментариях.

Все актуальные лучшие практики программирования можно освоить на онлайн‑курсах OTUS: в каталоге можно посмотреть список всех программ, а в календаре — записаться на открытые уроки.

Теги:
Хабы:
+3
Комментарии2

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS

Истории