Слышали ли вы когда-то о шаблонах проектирования? Возможно вам приходилось видеть в коде на 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
Ближайшая дата курса
по запросу
Продолжительность
32 ак.часов
Стоимость обучения
72 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 Мб.
Разработка и внедрение ML-решений
Код курса
MLOPS
Ближайшая дата курса
Продолжительность
24 ак.часов
Стоимость обучения
54 000 руб.
Еще больше подробностей о тонкостях работы с разделяемым данными на реальных примерах из Data Science вы узнаете на наших образовательных курсах в лицензированном учебном центре обучения и повышения квалификации руководителей и ИТ-специалистов (менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data) в Москве:
- DPREP: Подготовка данных для Data Mining на Python
- Разработка и внедрение ML-решений
- Графовые алгоритмы. Бизнес-приложения