Вопрос-Ответ

Usage of __slots__?

Использование __слотов__?

Какова цель __slots__ в Python — особенно в отношении того, когда я хотел бы его использовать, а когда нет?

Переведено автоматически
Ответ 1

Какова цель __slots__ в Python и в каких случаях этого следует избегать?


TLDR:

Специальный атрибут __slots__ позволяет вам явно указать, какие атрибуты экземпляра вы ожидаете от экземпляров вашего объекта, с ожидаемыми результатами:


  1. более быстрый доступ к атрибутам.

  2. экономия места в памяти.

Экономия места достигается за счет


  1. Хранение ссылок на значения в слотах вместо __dict__.

  2. Отрицание __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 + 27216 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__, чтобы сэкономить немного памяти.

python