Атрибуты, словари и слоты в Python
Понимание атрибутов в Python
Python по своей природе является очень динамичным языком. Переменные не нужно объявлять, их можно добавлять в качестве атрибутов практически везде. Рассмотрим пустой класс, например, такой:
class MyClass:
pass
Это полное определение класса в Python. Конечно, оно ничего не делает, но все же является верным.
В любой более поздний момент времени мы можем "наложить" атрибуты на наш класс следующим образом:
MyClass.class_attribute = 42
Класс имеет это новое class_attribute
значение с этого момента.
Если мы инстанцируем этот класс с помощью my_object = MyClass()
, мы можем убедиться, что значение class_attribute
равно 42:
>>> my_object.class_attribute
42
Конечно, мы также можем добавить атрибуты к нашим экземплярам:
>>> my_object.instance_attribute = 21
>>> my_object.instance_attribute
21
Вы когда-нибудь задумывались, где хранятся эти атрибуты?
Явное лучше неявного. (из Zen of Python)
Python не был бы Python без четко определенного и настраиваемого поведения для атрибутов. Атрибуты "items" в Python хранятся в магическом атрибуте под названием __dict__
. Мы можем получить к нему доступ следующим образом:
class MyClass:
class_attribute = "Class"
def __init__(self):
self.instance_attribute = "Instance"
my_object = MyClass()
print(my_object.__dict__)
print(MyClass.__dict__)
Как видите, class_attribute
хранится в __dict__
самого MyClass
, тогда как instance_attribute
хранится в __dict__
самого my_object
.
Это означает, что при каждом обращении к my_object.instance_attribute
Python будет искать сначала в my_object.__dict__
, а затем в MyClass.__dict__
. Если атрибут instance_attribute
не будет найден ни в одном из словарей, то возникнет ошибка AttributeError
.
Побочная заметка
Что такое "item" в Python? Вы видите, что каждая "item" в Python имеет атрибут __dict__
, даже сам класс. Логически, класс типа MyClass
имеет тип class
, что означает, что сам класс является объектом типа class
. Поскольку это может показаться непонятным, я использую разговорный термин "item".
"Взлом" атрибута __dict__
Как всегда в Python, атрибут __dict__
ведет себя как любой другой атрибут в Python. Поскольку Python - язык, предпочитающий передачу по ссылке, мы можем рассмотреть ошибку, которая встречается довольно часто и случайно. Рассмотрим класс AddressBook
:
class AddressBook:
addresses = []
Теперь давайте создадим несколько адресных книг и создадим несколько адресов:
alices_address_book = AddressBook()
alices_address_book.addresses.append(("Sherlock Holmes", "221B Baker St., London"))
alices_address_book.addresses.append(("Al Bundy", "9764 Jeopardy Lane, Chicago, Illinois"))
bobs_address_book = AddressBook()
bobs_address_book.addresses.append(("Bart Simpson", "742 Evergreen Terrace, Springfield, USA"))
bobs_address_book.addresses.append(("Hercule Poirot", "Apt. 56B, Whitehaven Mansions, Sandhurst Square, London W1"))
Интересно, что Элис и Боб теперь имеют одну адресную книгу:
>>> 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 Mansions, Sandhurst Square, London W1')]
>>> 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 Mansions, Sandhurst Square, London W1')]
Это происходит потому, что атрибут addresses
определен на уровне class
. Пустой список создается только один раз (addresses = []
), а именно, когда интерпретатор Python создает класс. Таким образом, для любого последующего экземпляра класса AddressBook
тот же list
будет ссылаться на addresses
. Мы можем исправить эту ошибку, перенеся создание пустого list
на уровень экземпляра следующим образом:
class AddressBook:
def __init__(self):
self.addresses = []
Переместив создание пустого списка в конструктор (метод __init__
), новый список создается каждый раз, когда создается новый экземпляр AddressBook
. Таким образом, экземпляры больше не будут непреднамеренно использовать один и тот же list
.
Знакомство с Боргом
Можем ли мы как-то специально использовать это поведение? Существует ли сценарий использования, когда мы хотим, чтобы все экземпляры использовали одно и то же хранилище? Оказывается, есть! Существует паттерн проектирования , называемый синглтон . Это гарантирует, что во время выполнения программы существует только один экземпляр класса. Например, это может быть полезно, если используется для класса соединения с базой данных или хранилища конфигурации.
Обратите внимание на то, что вы должны использовать синглтонные классы только иногда, потому что они вводят некоторое глобальное состояние в вашу программу, что затрудняет изолированное тестирование отдельных компонентов вашей программы.
Каким образом в Pythonic можно реализовать паттерн singleton?
Рассмотрим этот класс:
class Borg:
_shared = {}
def __init__(self):
self.__dict__ = self._shared
Этот класс имеет атрибут _shared
, инициализированный как пустой массив. Из предыдущих параграфов мы знаем, что экземпляр dict
является тем же объектом для класса . Тогда внутри конструктора (__init__
) мы устанавливаем __dict__
экземпляра в этот общий словарь. В результате все динамически добавляемые атрибуты становятся общими для каждого экземпляра этого класса.
Проверим:
>>> borg_1 = Borg()
>>> borg_2 = Borg()
>>>
>>> borg_1.value = 42
>>> borg_2.value
42
Почему мы не можем установить __dict__ = {}
непосредственно в класс, как например
class Borg:
__dict__ = {}
>>> borg_1 = Borg()
>>> borg_2 = Borg()
>>>
>>> borg_1.value = 42
>>> borg_2.value
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Borg' object has no attribute 'value'
Это связано с тем, что в последнем случае мы задаем атрибут __dict__
самому классу class. Однако мы получаем доступ к атрибуту instance, набирая borg_2.value
. Только когда атрибут __dict__
установлен на уровне instance, мы можем использовать наш шаблон Борга. Этого можно добиться, используя конструктор для изменения атрибута __dict__
на уровне экземпляра.
Использование памяти для атрибутов
Динамическое добавление атрибутов во время выполнения на уровне экземпляра или класса сопряжено с определенными затратами. Структура словаря занимает много памяти во внутреннем пространстве Python. В ситуациях, когда вы создаете много (тысячи) экземпляров, это может стать узким местом.
Однако, сначала о главном: Что такое слоты? Хотя в Python вы можете динамически добавлять атрибуты к "вещам", слоты ограничивают эту функциональность. Когда вы добавляете __slots__
атрибут к class
, вы предварительно определяете, какие атрибуты-члены вы разрешаете. Давайте посмотрим:
class SlottedClass:
__slots__ = ['value']
def __init__(self, i):
self.value = i
При таком определении любой экземпляр SlottedClass
может обращаться только к атрибуту value
. Доступ к другим (динамическим) атрибутам вызовет ошибку AttributeError
:
>>> slotted = SlottedClass(42)
>>> slotted.value
42
>>> slotted.forbidden_value = 21
AttributeError: 'SlottedClass' object has no attribute 'forbidden_value'
Ограничение возможности динамического добавления атрибутов полезно для уменьшения ошибок во время выполнения, которые могут возникнуть из-за опечаток в именах атрибутов. Однако, что более важно, это ограничение уменьшит использование памяти вашим кодом - в некоторых случаях значительно. Давайте попробуем проверить это.
Мы создаем два класса, один щелевой и один нещелевой. Оба класса обращаются к атрибуту с именем value
внутри своего метода __init__
, и в случае класса со слотом это единственный атрибут в __slots__
.
Мы создаем миллион экземпляров для каждого класса и храним эти экземпляры в списке. После этого мы смотрим на размер списка. Список экземпляров класса со щелью должен быть меньше.
import sys
class SlottedClass:
__slots__ = ['value']
def __init__(self, i):
self.value = i
class UnSlottedClass:
def __init__(self, i):
self.value = i
slotted = []
for i in range(1_000_000):
slotted.append(SlottedClass(i))
print(sys.getsizeof(slotted))
unslotted = []
for i in range(1_000_000):
unslotted.append(UnSlottedClass(i))
print(sys.getsizeof(unslotted))
Однако для каждого списка мы получаем обратно значение 8448728
. Как же нам сэкономить память, используя слоты?
Давайте воспользуемся модулем ipython-memory-usage, чтобы проверить, сколько памяти потребляется во время выполнения нашей тестовой программы.
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 Мб, хотя результирующий размер списков одинаков.
Причина этого заключается в том, как Python внутренне обрабатывает dict
. Если не указывать __slots__
, Python по умолчанию использует словарь для хранения атрибутов. Этот словарь динамичен по своей природе, может изменяться в размерах, должен быть организован по ключам и т.д. Поэтому Python требуется много памяти для управления словарем.
В щелевой версии класса ключевые характеристики dict
больше не нужны, поскольку динамическое изменение размера больше не допускается. Таким образом, Python заранее выделяет память для атрибутов, упомянутых в __slots__
.
https://bas.codes/posts/python-dict-slots
Вернуться на верх