Использование __слотов__?
Какова цель __slots__
в Python — особенно в отношении того, когда я хотел бы его использовать, а когда нет?
Переведено автоматически
Ответ 1
Какова цель
__slots__
в Python и в каких случаях этого следует избегать?
TLDR:
Специальный атрибут __slots__
позволяет вам явно указать, какие атрибуты экземпляра вы ожидаете от экземпляров вашего объекта, с ожидаемыми результатами:
- более быстрый доступ к атрибутам.
- экономия места в памяти.
Экономия места достигается за счет
- Хранение ссылок на значения в слотах вместо
__dict__
. - Отрицание
__dict__
и__weakref__
создание, если родительские классы отрицают их и вы объявляете__slots__
.
Краткие предостережения
Небольшое предостережение, вы должны объявлять определенный слот только один раз в дереве наследования. Например:
class Base:
__slots__ = 'foo', 'bar'
class Right(Base):
__slots__ = 'baz',
class Wrong(Base):
__slots__ = 'foo', 'bar', 'baz' # redundant foo and bar
Python не возражает, когда вы ошибаетесь (вероятно, так и должно быть), иначе проблемы могут и не проявиться, но ваши объекты будут занимать больше места, чем следовало бы. Python 3.8:
>>> from sys import getsizeof
>>> getsizeof(Right()), getsizeof(Wrong())
(56, 72)
Это потому, что дескриптор базового слота имеет слот, отдельный от неправильного. Обычно это не должно возникать, но может возникнуть:
>>> w = Wrong()
>>> w.foo = 'foo'
>>> Base.foo.__get__(w)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: foo
>>> Wrong.foo.__get__(w)
'foo'
Самое большое предостережение касается множественного наследования - несколько "родительских классов с непустыми слотами" не могут быть объединены.
Чтобы учесть это ограничение, следуйте рекомендациям: исключите все родительские абстракции, кроме одной или всех, от которых унаследует их конкретный класс соответственно и ваш новый конкретный класс в совокупности - предоставив абстракции пустые слоты (точно так же, как абстрактным базовым классам в стандартной библиотеке).
Смотрите раздел о множественном наследовании ниже для примера.
Требования:
Чтобы атрибуты, названные в
__slots__
, действительно хранились в слотах вместо__dict__
, класс должен наследоваться отobject
(автоматически в Python 3, но должно быть явно в Python 2).Чтобы предотвратить создание
__dict__
, вы должны наследовать отobject
и все классы в наследовании должны объявляться__slots__
и ни у одного из них не может быть'__dict__'
записи.
Здесь много подробностей, если вы хотите продолжить чтение.
Зачем использовать __slots__
: Более быстрый доступ к атрибутам.
Создатель Python, Гвидо ван Россум, заявляет, что на самом деле он создал его __slots__
для более быстрого доступа к атрибутам.
Продемонстрировать значительно более быстрый доступ - тривиально:
import timeit
class Foo(object): __slots__ = 'foo',
class Bar(object): pass
slotted = Foo()
not_slotted = Bar()
def get_set_delete_fn(obj):
def get_set_delete():
obj.foo = 'foo'
obj.foo
del obj.foo
return get_set_delete
и
>>> min(timeit.repeat(get_set_delete_fn(slotted)))
0.2846834529991611
>>> min(timeit.repeat(get_set_delete_fn(not_slotted)))
0.3664822799983085
Доступ по слотам почти на 30% быстрее в Python 3.5 в Ubuntu.
>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342
В Python 2 в Windows я измерил, что он работает примерно на 15% быстрее.
Зачем использовать __slots__
: экономия памяти
Другая цель __slots__
- уменьшить пространство в памяти, которое занимает каждый экземпляр объекта.
В моем собственном вкладе в документацию четко указаны причины этого:
Экономия места при использовании
__dict__
может быть значительной.
SQLAlchemy приписывает большую экономию памяти __slots__
.
Чтобы убедиться в этом, используя дистрибутив Anaconda Python 2.7 для Ubuntu Linux, с guppy.hpy
(он же heapy) и sys.getsizeof
, размер экземпляра класса без __slots__
объявленного, и ничего больше, равен 64 байтам. Это не включает __dict__
. Еще раз спасибо Python за ленивую оценку, __dict__
по-видимому, не вызывается к существованию, пока на него не ссылаются, но классы без данных обычно бесполезны. При вызове __dict__
атрибут имеет дополнительный размер не менее 280 байт.
Напротив, экземпляр класса с __slots__
объявленным как ()
(без данных) имеет размер всего 16 байт, а всего 56 байт с одним элементом в слотах, 64 с двумя.
Для 64-битного Python я иллюстрирую потребление памяти в байтах в Python 2.7 и 3.6 для __slots__
и __dict__
(слоты не определены) для каждой точки, где значение dict увеличивается в 3.6 (за исключением атрибутов 0, 1 и 2):
Python 2.7 Python 3.6
attrs __slots__ __dict__* __slots__ __dict__* | *(no slots defined)
none 16 56 + 272† 16 56 + 112† | †if __dict__ referenced
one 48 56 + 272 48 56 + 112
two 56 56 + 272 56 56 + 112
six 88 56 + 1040 88 56 + 152
11 128 56 + 1040 128 56 + 240
22 216 56 + 3344 216 56 + 408
43 384 56 + 3344 384 56 + 752
Итак, несмотря на меньшие значения в Python 3, мы видим, насколько хорошо __slots__
масштабируются экземпляры для экономии памяти, и это основная причина, по которой вы хотели бы использовать __slots__
.
Просто для полноты моих заметок обратите внимание, что единовременная стоимость каждого слота в пространстве имен класса составляет 64 байта в Python 2 и 72 байта в Python 3, потому что слоты используют дескрипторы данных, такие как свойства, называемые "членами".
>>> Foo.foo
<member 'foo' of 'Foo' objects>
>>> type(Foo.foo)
<class 'member_descriptor'>
>>> getsizeof(Foo.foo)
72
Демонстрация __slots__
:
Чтобы запретить создание __dict__
, вы должны создать подкласс object
. В Python 3 все имеет подклассы object
, но в Python 2 вы должны были быть явными:
class Base(object):
__slots__ = ()
теперь:
>>> b = Base()
>>> b.a = 'a'
Traceback (most recent call last):
File "<pyshell#38>", line 1, in <module>
b.a = 'a'
AttributeError: 'Base' object has no attribute 'a'
Или подкласс другого класса, который определяет __slots__
class Child(Base):
__slots__ = ('a',)
а теперь:
c = Child()
c.a = 'a'
но:
>>> c.b = 'b'
Traceback (most recent call last):
File "<pyshell#42>", line 1, in <module>
c.b = 'b'
AttributeError: 'Child' object has no attribute 'b'
Чтобы разрешить __dict__
создание при создании подклассов объектов с прорезями, просто добавьте '__dict__'
в __slots__
(обратите внимание, что слоты упорядочены, и вы не должны повторять слоты, которые уже есть в родительских классах):
class SlottedWithDict(Child):
__slots__ = ('__dict__', 'b')
swd = SlottedWithDict()
swd.a = 'a'
swd.b = 'b'
swd.c = 'c'
и
>>> swd.__dict__
{'c': 'c'}
Или вам даже не нужно объявлять __slots__
в вашем подклассе, и вы по-прежнему будете использовать слоты от родителей, но не ограничивать создание __dict__
:
class NoSlots(Child): pass
ns = NoSlots()
ns.a = 'a'
ns.b = 'b'
И:
>>> ns.__dict__
{'b': 'b'}
Однако, __slots__
может вызвать проблемы при множественном наследовании:
class BaseA(object):
__slots__ = ('a',)
class BaseB(object):
__slots__ = ('b',)
Поскольку создание дочернего класса из родительского с обоими непустыми слотами завершается неудачей:
>>> class Child(BaseA, BaseB): __slots__ = ()
Traceback (most recent call last):
File "<pyshell#68>", line 1, in <module>
class Child(BaseA, BaseB): __slots__ = ()
TypeError: Error when calling the metaclass bases
multiple bases have instance lay-out conflict
Если вы столкнетесь с этой проблемой, вы могли бы просто удалить __slots__
из родительских файлов, или, если у вас есть контроль над родительскими файлами, предоставить им пустые слоты, или реорганизовать абстракции:
from abc import ABC
class AbstractA(ABC):
__slots__ = ()
class BaseA(AbstractA):
__slots__ = ('a',)
class AbstractB(ABC):
__slots__ = ()
class BaseB(AbstractB):
__slots__ = ('b',)
class Child(AbstractA, AbstractB):
__slots__ = ('a', 'b')
c = Child() # no problem!
Добавьте '__dict__'
в __slots__
, чтобы получить динамическое назначение:
class Foo(object):
__slots__ = 'bar', 'baz', '__dict__'
а теперь:
>>> foo = Foo()
>>> foo.boink = 'boink'
Итак, с '__dict__'
в слотах мы теряем некоторые преимущества в размере из-за возможности динамического присваивания и сохранения слотов для ожидаемых имен.
Когда вы наследуете от объекта, у которого нет слотов, вы получаете ту же семантику, когда используете __slots__
- имена, которые находятся в __slots__
, указывают на значения со слотами, в то время как любые другие значения помещаются в экземпляре __dict__
.
Избегать этого, __slots__
потому что вы хотите иметь возможность добавлять атрибуты "на лету", на самом деле не является веской причиной - просто добавьте "__dict__"
в свой __slots__
, если это требуется.
Вы можете аналогичным образом добавить __weakref__
в __slots__
явно, если вам нужна эта функция.
Устанавливается значение пустой кортеж при создании подкласса namedtuple:
Встроенный namedtuple создает неизменяемые экземпляры, которые очень легкие (по сути, размер кортежей), но чтобы получить преимущества, вам нужно сделать это самостоятельно, если вы создадите их подкласс:
from collections import namedtuple
class MyNT(namedtuple('MyNT', 'bar baz')):
"""MyNT is an immutable and lightweight object"""
__slots__ = ()
использование:
>>> nt = MyNT('bar', 'baz')
>>> nt.bar
'bar'
>>> nt.baz
'baz'
И попытка присвоить неожиданный атрибут вызывает AttributeError
потому что мы предотвратили создание __dict__
:
>>> nt.quux = 'quux'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyNT' object has no attribute 'quux'
Вы можете разрешить __dict__
создание, отключив __slots__ = ()
, но вы не можете использовать непустой __slots__
с подтипами кортежа.
Самое большое предостережение: множественное наследование
Даже если непустые слоты одинаковы для нескольких родительских элементов, их нельзя использовать вместе:
class Foo(object):
__slots__ = 'foo', 'bar'
class Bar(object):
__slots__ = 'foo', 'bar' # alas, would work if empty, i.e. ()
>>> class Baz(Foo, Bar): pass
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Error when calling the metaclass bases
multiple bases have instance lay-out conflict
Использование пустого __slots__
элемента в родительском элементе, по-видимому, обеспечивает наибольшую гибкость, позволяя дочернему элементу выбирать, предотвращать или разрешать (добавляя '__dict__'
для получения динамического назначения, см. Раздел выше) создание __dict__
:
class Foo(object): __slots__ = ()
class Bar(object): __slots__ = ()
class Baz(Foo, Bar): __slots__ = ('foo', 'bar')
b = Baz()
b.foo, b.bar = 'foo', 'bar'
Вам не обязательно иметь слоты - поэтому, если вы добавите их и удалите позже, это не должно вызвать никаких проблем.
Здесь мы рискуем: если вы составляете миксины или используете абстрактные базовые классы, которые не предназначены для создания экземпляров, пустое значение __slots__
в этих родительских элементах кажется лучшим способом с точки зрения гибкости для подклассов.
Для демонстрации сначала давайте создадим класс с кодом, который мы хотели бы использовать при множественном наследовании
class AbstractBase:
__slots__ = ()
def __init__(self, a, b):
self.a = a
self.b = b
def __repr__(self):
return f'{type(self).__name__}({repr(self.a)}, {repr(self.b)})'
Мы могли бы использовать вышесказанное напрямую, унаследовав и объявив ожидаемые слоты:
class Foo(AbstractBase):
__slots__ = 'a', 'b'
Но нас это не волнует, это тривиальное одиночное наследование, нам нужен другой класс, от которого мы также могли бы наследовать, возможно, с атрибутом noisy:
class AbstractBaseC:
__slots__ = ()
@property
def c(self):
print('getting c!')
return self._c
@c.setter
def c(self, arg):
print('setting c!')
self._c = arg
Теперь, если бы в обеих базах были непустые слоты, мы не смогли бы сделать нижеприведенное. (На самом деле, если бы мы хотели, мы могли бы указать AbstractBase
непустые слоты a и b и исключить их из приведенного ниже объявления - оставлять их было бы неправильно):
class Concretion(AbstractBase, AbstractBaseC):
__slots__ = 'a b _c'.split()
И теперь у нас есть функциональность от обоих через множественное наследование, и мы все еще можем запретить __dict__
и __weakref__
создание экземпляра:
>>> c = Concretion('a', 'b')
>>> c.c = c
setting c!
>>> c.c
getting c!
Concretion('a', 'b')
>>> c.d = 'd'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Concretion' object has no attribute 'd'
Другие случаи, когда следует избегать слотов:
- Избегайте их, если вы хотите выполнить
__class__
назначение с другим классом, в котором их нет (и вы не можете их добавить), если только макеты слотов не идентичны. (Мне очень интересно узнать, кто это делает и почему.) - Избегайте их, если вы хотите создать подкласс встроенных объектов переменной длины, таких как long, tuple или str, и хотите добавить к ним атрибуты.
- Избегайте их, если вы настаиваете на предоставлении значений по умолчанию через атрибуты класса для переменных экземпляра.
Возможно, вы сможете извлечь дополнительные предостережения из остальной __slots__
документации (документы 3.7 dev являются самыми актуальными), в которую я недавно внес значительный вклад.
Критика других ответов
Текущие лучшие ответы содержат устаревшую информацию, довольно размыты от руки и в некоторых важных аспектах не попадают в цель.
Не "используйте только __slots__
при создании множества объектов"
Цитирую:
"Вы хотели бы использовать
__slots__
, если собираетесь создавать экземпляры большого количества (сотен, тысяч) объектов одного и того же класса".
Абстрактные базовые классы, например, из collections
модуля, не создаются, но __slots__
для них объявлены.
Почему?
Если пользователь желает запретить __dict__
или __weakref__
создание, эти функции не должны быть доступны в родительских классах.
__slots__
способствует возможности повторного использования при создании интерфейсов или миксинов.
Это правда, что многие пользователи Python пишут не для повторного использования, но когда это так, наличие возможности запретить ненужное использование пространства является ценным.
__slots__
не нарушает травление
При обработке объекта с прорезями вы можете обнаружить, что он выдает вводящий в заблуждениеTypeError
:
>>> pickle.loads(pickle.dumps(f))
TypeError: a class that defines __slots__ without defining __getstate__ cannot be pickled
На самом деле это неверно. Это сообщение исходит от самого старого протокола, который используется по умолчанию. Вы можете выбрать самый последний протокол с помощью аргумента -1
. В Python 2.7 это было бы 2
(который был представлен в 2.3), а в 3.6 это так 4
.
>>> pickle.loads(pickle.dumps(f, -1))
<__main__.Foo object at 0x1129C770>
в Python 2.7:
>>> pickle.loads(pickle.dumps(f, 2))
<__main__.Foo object at 0x1129C770>
в Python 3.6
>>> pickle.loads(pickle.dumps(f, 4))
<__main__.Foo object at 0x1129C770>
Итак, я бы держал это в уме, поскольку это решенная проблема.
Критика принятого ответа (до 2 октября 2016 г.)
Первый абзац представляет собой наполовину краткое объяснение, наполовину прогностический. Вот единственная часть, которая действительно отвечает на вопрос
Правильное использование
__slots__
заключается в экономии места в объектах. Вместо динамического dict, который позволяет добавлять атрибуты к объектам в любое время, существует статическая структура, которая не допускает добавления после создания. Это экономит накладные расходы на один dict для каждого объекта, использующего слоты
Вторая половина выдает желаемое за действительное и не соответствует действительности:
Хотя иногда это полезная оптимизация, в ней не было бы никакой необходимости, если бы интерпретатор Python был достаточно динамичным, чтобы он требовал dict только тогда, когда к объекту действительно были дополнения.
Python на самом деле делает нечто подобное, только создавая __dict__
при обращении к нему, но создавать множество объектов без данных довольно нелепо.
Второй абзац чрезмерно упрощает и упускает реальные причины , которых следует избегать __slots__
. Приведенное ниже не является реальной причиной избегать слотов (о реальных причинах см. Остальную часть моего ответа выше.):
Они изменяют поведение объектов, у которых есть слоты, таким образом, что ими могут злоупотреблять любители управления и слабаки от статической типизации.
Затем далее обсуждаются другие способы достижения этой извращенной цели с помощью Python, не обсуждая ничего общего с __slots__
.
Третий абзац скорее выдает желаемое за действительное. В совокупности это в основном нестандартный контент, автором которого даже не был ответчик, и он служит аргументом для критиков сайта.
Доказательства использования памяти
Создайте несколько обычных объектов и объектов с прорезями:
>>> class Foo(object): pass
>>> class Bar(object): __slots__ = ()
Создайте миллион из них:
>>> foos = [Foo() for f in xrange(1000000)]
>>> bars = [Bar() for b in xrange(1000000)]
Проверять с помощью guppy.hpy().heap()
:
>>> guppy.hpy().heap()
Partition of a set of 2028259 objects. Total size = 99763360 bytes.
Index Count % Size % Cumulative % Kind (class / dict of class)
0 1000000 49 64000000 64 64000000 64 __main__.Foo
1 169 0 16281480 16 80281480 80 list
2 1000000 49 16000000 16 96281480 97 __main__.Bar
3 12284 1 987472 1 97268952 97 str
...
Получите доступ к обычным объектам и их __dict__
и снова проверьте:
>>> for f in foos:
... f.__dict__
>>> guppy.hpy().heap()
Partition of a set of 3028258 objects. Total size = 379763480 bytes.
Index Count % Size % Cumulative % Kind (class / dict of class)
0 1000000 33 280000000 74 280000000 74 dict of __main__.Foo
1 1000000 33 64000000 17 344000000 91 __main__.Foo
2 169 0 16281480 4 360281480 95 list
3 1000000 33 16000000 4 376281480 99 __main__.Bar
4 12284 0 987472 0 377268952 99 str
...
Это согласуется с историей Python, начиная с унификации типов и классов в Python 2.2
Если вы создаете подкласс встроенного типа, к экземплярам автоматически добавляется дополнительное пространство для размещения
__dict__
и__weakrefs__
. (__dict__
не инициализируется до тех пор, пока вы его не используете, поэтому вам не стоит беспокоиться о пространстве, занимаемом пустым словарем для каждого создаваемого вами экземпляра.) Если вам не нужно это дополнительное пространство, вы можете добавить фразу "__slots__ = []
" в свой класс.
Ответ 2
Цитирую Джейкоба Халлена:
Правильное использование
__slots__
заключается в экономии места в объектах. Вместо динамического dict, который позволяет добавлять атрибуты к объектам в любое время, существует статическая структура, которая не допускает добавления после создания. [Такое использование__slots__
устраняет накладные расходы на один dict для каждого объекта.] Хотя иногда это полезная оптимизация, в ней не было бы никакой необходимости, если бы интерпретатор Python был достаточно динамичным, чтобы он требовал dict только тогда, когда к объекту действительно были дополнения.К сожалению, у слотов есть побочный эффект. Они изменяют поведение объектов, у которых есть слоты, таким образом, что ими могут злоупотреблять любители управления и статической типизации. Это плохо, потому что помешанные на управлении должны злоупотреблять метаклассами, а любители статической типизации должны злоупотреблять декораторами, поскольку в Python должен быть только один очевидный способ сделать что-либо.
Сделать CPython достаточно умным, чтобы справляться с экономией места без
__slots__
- серьезная задача, вероятно, поэтому его нет в списке изменений для P3k (пока).
Ответ 3
Вы хотели бы использовать __slots__
, если собираетесь создавать экземпляры большого количества (сотен, тысяч) объектов одного класса. __slots__
существует только как инструмент оптимизации памяти.
Настоятельно не рекомендуется использовать __slots__
для ограничения создания атрибута.
Травление объектов с помощью __slots__
не будет работать с протоколом pickle по умолчанию (самым старым); необходимо указать более позднюю версию.
Некоторые другие функции самоанализа python также могут быть затронуты неблагоприятным образом.
Ответ 4
У каждого объекта python есть __dict__
атрибут attribute, который представляет собой словарь, содержащий все остальные атрибуты. например, когда вы вводите, self.attr
python действительно выполняет self.__dict__['attr']
. Как вы можете себе представить, использование словаря для хранения атрибута требует некоторого дополнительного пространства и времени для доступа к нему.
Однако при использовании __slots__
любой объект, созданный для этого класса, не будет иметь __dict__
атрибута. Вместо этого доступ ко всем атрибутам осуществляется напрямую через указатели.
Итак, если вам нужна структура в стиле C, а не полноценный класс, вы можете использовать __slots__
для уменьшения размера объектов и сокращения времени доступа к атрибутам. Хорошим примером является класс Point, содержащий атрибуты x и y. Если вы собираетесь набрать много очков, вы можете попробовать использовать __slots__
, чтобы сэкономить немного памяти.