Слышали ли вы когда-то о шаблонах проектирования? Возможно вам приходилось видеть в коде на Python атрибуты __dict__
и __slots__
. В этой статье разберемся, что это за атрибуты и как они используются в шаблонах проектирования.
Что за атрибуты в Python
Python — это очень динамический язык по своей натуре. Переменные (правильнее было бы сказать идентификаторы) не нужно объявлять и могут быть добавлены как атрибуты в любом месте. Рассмотрим пустой класс:
class MyClass: pass
В любом месте программы мы можем через класс создать атрибут, например, так:
MyClass.class_attribute = 42
Теперь у класса есть атрибут class_attribute
. Можно это проверить через инстанциирование класса:
>>> my_object = MyClass() >>> my_object.class_attribute 42
Код курса
DPREP
Ближайшая дата курса
по запросу
Продолжительность
ак.часов
Стоимость обучения
0 руб.
Конечно, мы также можем создать атрибут для конкретного объекта:
>>> 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-решений
- Графовые алгоритмы. Бизнес-приложения