Зачем в Python используется __dict__ и __slots__

Слышали ли вы когда-то о шаблонах проектирования? Возможно вам приходилось видеть в коде на Python атрибуты __dict__ и __slots__. В этой статье разберемся, что это за атрибуты и как они используются в шаблонах проектирования.

Что за атрибуты в Python

Python — это очень динамический язык по своей натуре. Переменные (правильнее было бы сказать идентификаторы) не нужно объявлять и могут быть добавлены как атрибуты в любом месте. Рассмотрим пустой класс:

class MyClass:
    pass

В любом месте программы мы можем через класс создать атрибут, например, так:

MyClass.class_attribute = 42

Теперь у класса есть атрибут class_attribute. Можно это проверить через инстанциирование класса:

>>> my_object = MyClass()
>>> my_object.class_attribute
42

Подготовка данных для Data Mining на Python

Код курса
DPREP
Ближайшая дата курса
31 октября, 2022
Длительность обучения
32 ак.часов
Стоимость обучения
60 000 руб.

Конечно, мы также можем создать атрибут для конкретного объекта:

>>> my_object.instance_attribute = 21
>>> my_object.instance_attribute
21

Разберемся, как это работает подробней.

Каким образом в Python хранятся атрибуты

Атрибуты в Python хранятся в магическом атрибуте __dict__. Можем получить к нему доступ:

class MyClass:
    class_attribute = "Class"

    def __init__(self):
        self.instance_attribute = "Instance"

my_object = MyClass()

print(my_object.__dict__)
print(MyClass.__dict__)

"""
{'instance_attribute': 'Instance'}
{'__module__': '__main__', 'class_attribute': 'Class', '__init__': <function MyClass.__init__ at 0x7f57d910d280>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None}
"""

Как можно заметить, class_attribute хранится в словаре всего класса, а instance_attribute хранится для объекта. Действительно, конструктор создается именно для объекта.

Это означает, что все в Python имеет атрибут __dict__, включая классы и функции. Кроме того, нужно понимать, что всё в Python — это объект, к которому можно обращаться.

Что такое __dict__

__dict__ ведет себя так же, как и все атрибуты в Python. Давайте рассмотрим проблему изменяемых переменных на примере списка:

class AddressBook:
    addresses = []

Создадим несколько экземпляров этого класса и заполним список. Код на Python:

alices = AddressBook()
alices.addresses.append(("Sherlock Holmes", "221B Baker St., London"))
alices.addresses.append(("Al Bundy", "9764 Jeopardy Lane, Chicago, Illinois"))


bobs= AddressBook()
bobs.addresses.append(("Bart Simpson", "742 Evergreen Terrace, Springfield, USA"))
bobs.addresses.append(("Hercule Poirot", "Apt. 56B, Whitehaven"))

Теперь эти объекты делят один список на двоих:

>>> alices_address_book.addresses
[('Sherlock Holmes', '221B Baker St., London'),
 ('Al Bundy', '9764 Jeopardy Lane, Chicago, Illinois'),
 ('Bart Simpson', '742 Evergreen Terrace, Springfield, USA'),
 ('Hercule Poirot', 'Apt. 56B, Whitehaven')]
>>> bobs_address_book.addresses
[('Sherlock Holmes', '221B Baker St., London'),
 ('Al Bundy', '9764 Jeopardy Lane, Chicago, Illinois'),
 ('Bart Simpson', '742 Evergreen Terrace, Springfield, USA'),
 ('Hercule Poirot', 'Apt. 56B, Whitehaven')]

Это происходит потому, что список определен на уровне класса. Пустой список создается только один раз: в тот момент когда интерпретатор создает этот самый класс. Кроме того, список является изменяемым объектом (mutable), о них мы говорили в этой статье.

Мы можем создавать список на уровне экземпляра класса, добавив его в конструктор:

class AddressBook:
    def __init__(self):
        self.addresses = []

Теперь список будет создаваться в момент инстанциирования, т.е. когда создается экземпляр класса.

Введение в шаблон проектирования Borg

Можем ли мы где-то использовать данное поведение? Есть ли случаи, когда мы хотим, чтобы экземпляры класса располагали одним хранилищем? Такой случай есть, и он даже имеет название — Singleton, один их шаблонов проектирования. Однако есть более усовершенствованная версия под названием Borg (различие заключается в том, подклассы Borg’а разделяют ту же память, что и родительский класс [1]). Он может пригодиться, например, в классах с соединениями баз данных или с конфигурационным хранилищем.

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

Как реализуется Borg

Рассмотрим такой класс:

class Borg:
    _shared = {}
    def __init__(self):
        self.__dict__ = self._shared

В классе есть атрибут _shared, который инициализирован на уровне класса в виде пустого словаря. Внутри конструктора мы присвоили __dict__ этот словарь. В результате во время выполнения все добавленные атрибуты станут разделяемой областью памяти.

Проверим это:

>>> borg_1 = Borg()
>>> borg_2 = Borg()
>>> 
>>> borg_1.value = 42
>>> borg_2.value 
42

Зачем нужен атрибут __slots__

Динамическое добавление атрибутов в во время выполнения стоит дорого. Кроме того, мы используем еще и словари, которые неплохо нагружают память. Поэтому если у вас тысячи экземпляров, то готовьтесь исправлять код.

В Python есть еще один магический атрибут под названием __slots__, который ограничивает добавляемые атрибуты. Это позволяет сэкономить на памяти. Например, следующий код на Python:

class SlottedClass:
    __slots__ = ['value']

>>> s = SlottedClass
>>> s = 10
>>> s.other = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'SlottedClass' object has no attribute 'other'

— позволяет создавать только атрибуты с именем value. Другим плюсом данного подхода является избегание ошибок в правописании названия атрибута.

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

In [1]: def slotted_fn():
   ...:     class SlottedClass:
   ...:         __slots__ = ["value"]
   ...:
   ...:         def __init__(self, i):
   ...:             self.value = i
   ...:
   ...:     slotted = []
   ...:     for i in range(1_000_000):
   ...:         slotted.append(SlottedClass(i))
   ...:     return slotted
   ...:
   ...:
   ...: def unslotted_fn():
   ...:     class UnSlottedClass:
   ...:         def __init__(self, i):
   ...:             self.value = i
   ...:
   ...:     unslotted = []
   ...:     for i in range(1_000_000):
   ...:         unslotted.append(UnSlottedClass(i))
   ...:     return unslotted
   ...:
   ...:
   ...: import ipython_memory_usage.ipython_memory_usage as imu
   ...:
   ...: imu.start_watching_memory()
In [1] used 0.0000 MiB RAM in 0.11s, peaked 0.00 MiB above current, total RAM usage 52.48 MiB

In [2]: slotted_fn()
Out[2]: ...
In [2] used 84.9766 MiB RAM in 0.73s, peaked 0.00 MiB above current, total RAM usage 139.00 MiB

In [3]: unslotted_fn()
Out[3]: ...
In [3] used 200.1562 MiB RAM in 0.84s, peaked 0.00 MiB above current, total RAM usage 339.16 MiB

Таким образом, класс с заданными атрибутами охватил только 85 Мб, а без — 200 Мб.


Еще больше подробностей о тонкостях работы с разделяемым данными на реальных примерах из Data Science вы узнаете на наших образовательных курсах в лицензированном учебном центре обучения и повышения квалификации руководителей и ИТ-специалистов (менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data) в Москве:

Источники
  1. Borg vs Singleton

Добавить комментарий

Поиск по сайту