__slots__ Python

Введение

Официальная документация: docs , wiki

Статья на сайте SQLAlchemy - Significant Improvements in Structural Memory Use

Пример

Без использования __slots__ у каждого экземпляра класса есть свой собственный словарь атрибутов

class Developer: def __init__(self, name, age, salary, framework): self.name = name self.age = age self.salary = salary self.framework = framework employee1 = Developer("Andrei", 36, 5000, "FastAPI") print(employee1.__dict__) print(hasattr(employee1, "__slots__"))

{'name': 'Andrei', 'age': 36, 'salary': 5000, 'framework': 'FastAPI'} False

При использовании __slots__ отдельные словари не создаются

class Developer: __slots__ = ("name", "age", "salary", "framework") def __init__(self, name, age, salary, framework): self.name = name self.age = age self.salary = salary self.framework = framework employee1 = Developer("Andrei", 36, 5000, "FastAPI") print(employee1.__dict__)

Traceback (most recent call last): File "C:\Users\Andrei\slots_demo.py", line 13, in <module> print(employee1.__dict__) AttributeError: 'Developer' object has no attribute '__dict__'

Можно обратиться к атрибуту __slots__

… print(employee1.__slots__) print(hasattr(employee1, "__slots__"))

('name', 'age', 'salary', 'framework') True

Разница в поведении

Попробуем добавить аттрибут к обычному классу

class Regular: def __init__(self, name, age): self.name = name self.age = age regular = Regular("Eugene", 59) regular.city = "Benalmadena" print(regular.city)

Benalmadena

Если тоже самое проделать с классом, где используются __slots__ появится AttributeError

class Slotted: __slots__ = ("name", "age") def __init__(self, name, age): self.name = name self.age = age slotted = Slotted("Andrei", 36) slotted.city = "Malaga" print(slotted.city)

Traceback (most recent call last): File "C:\Users\Andrei\slots_demo.py", line 10, in <module> slotted.city = "Malaga" ^^^^^^^^^^^^ AttributeError: 'Slotted' object has no attribute 'city'

Проверка экономии памяти

Если теперь в оба примера (без __slots__ и со __slots__) добавить проверку размера объекта метдом asizeof из pympler, окажется, что объект, где применялся __slots__ больше

from pympler import asizeof … print(f"asizeof() slotted size: {asizeof.asizeof(slotted_emp)}") … print(f"asizeof() not slotted size: {asizeof.asizeof(not_slotted_emp)}")

asizeof() slotted size: 240 asizeof() not slotted size: 560

Экономия приличная, но если воспользоваться другим измерителем размера, результат сильно исказится, подробнее в статье «Размер объекта в Python»

Проверка скорости

Выясним есть ли ускорение при обращении к атрибутам

import timeit from sys import getsizeof class Developer: __slots__ = ("name", "age", "salary", "framework") def __init__(self, name, age, salary, framework): self.name = name self.age = age self.salary = salary self.framework = framework class Programmer: def __init__(self, name, age, salary, framework): self.name = name self.age = age self.salary = salary self.framework = framework slotted_emp = Developer("Andrei", 36, 5000, "FastAPI") not_slotted_emp = Programmer("Eugene", 59, 95000, "Spring") # print(employee1.__dict__) # print(employee1.__slots__) def get_set_delete_fn(obj): def get_set_del(): obj.name = "foo" obj.name del obj.name obj.age = "foo" obj.age del obj.age obj.salary = "foo" obj.salary del obj.salary return get_set_del print(f"min slotted: {min(timeit.repeat(get_set_delete_fn(slotted_emp)))}") print(f"max slotted: {max(timeit.repeat(get_set_delete_fn(slotted_emp)))}") print(f"min not slotted: {min(timeit.repeat(get_set_delete_fn(not_slotted_emp)))}") print(f"max not slotted: {max(timeit.repeat(get_set_delete_fn(not_slotted_emp)))}")

min slotted: 0.10821229999999998 max slotted: 0.11044899999999991 min not slotted: 0.14354210000000012 max not slotted: 0.14311980000000002

Ускорение от __slotted__ в нашем примере очевидно.

__slots__ при наследовании

В родительском классе в __slots__ указываются его атрибуты. В производном классе в __slots__ нужно перечислять только новые для этого класса атрибуты. Перечислять заново родительские не нужно.

class Employee: __slots__ = ("name", "age", "salary") def __init__(self, name, age, salary): self.name = name self.age = age self.salary = salary def increase_salary(self, percent): self.salary += self.salary * (percent/100) class Developer(Employee): __slots__ = ("prog_language", "framework",) def __init__(self, name, age, salary, prog_language, framework): super().__init__(name, age, salary) self.prog_language = prog_language self.framework = framework class Tester(Employee): __slots__ = ("test_framework",) def __init__(self, name, age, salary, test_framework): super().__init__(name, age, salary) self.test_framework = test_framework def run_tests(self): print(f"Testing is started by {self.name}...") print("Tests are done.") employee1 = Developer("Andrei", 36, 5000, "python", "FastAPI") employee2 = Tester("Cem", 36, 5000, "PyTest") print(employee1.salary) employee1.increase_salary(10) print(employee1.salary) employee2.run_tests()

5000 5500.0 Testing is started by Cem... Tests are done.

Недостатки

При использовании __slots__ пропадает возможность динамически добавлять атрибуты. Можно пользоваться только тем, что определено в __slots__

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

Автор статьи: Андрей Олегович