Использование __slots__?
Какова цель __slots__
в Python - особенно в отношении того, когда я хотел бы использовать его, а когда нет?
14 ответов
В Python, какова цель
__slots__
а в каких случаях нужно этого избегать?
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 не возражает, когда вы понимаете это неправильно (возможно, так и должно быть), иначе проблемы могут не проявиться, но ваши объекты займут больше места, чем в противном случае.
>>> from sys import getsizeof
>>> getsizeof(Right()), getsizeof(Wrong())
(64, 80)
Наибольшее предостережение касается множественного наследования - несколько "родительских классов с непустыми слотами" не могут быть объединены.
Чтобы учесть это ограничение, следуйте рекомендациям: Вычтите все абстракции всех родителей, кроме одного или всех, от которых наследует их конкретный класс, соответственно, и ваш новый конкретный класс - предоставляя абстракции (ям) пустые слоты (как абстрактные базовые классы в стандартная библиотека).
См. Раздел о множественном наследовании ниже для примера.
Требования:
Иметь атрибуты с именами в
__slots__
на самом деле храниться в слотах вместо__dict__
класс должен наследовать отobject
,Чтобы предотвратить создание
__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
(он же хиппи) и 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
:
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 делает неизменяемые экземпляры очень легкими (по сути, размером кортежей), но чтобы получить преимущества, вам нужно сделать это самостоятельно, если вы подклассируете их:
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), в которую я внес значительный вклад в последнее время.
Критика других ответов
Нынешние топ-ответы приводят устаревшую информацию и довольно волнисты, и в некоторых важных аспектах не попадают в цель.
Не "только использовать __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__
это сэкономить место в объектах. Вместо того, чтобы иметь динамический диктант, который позволяет добавлять атрибуты к объектам в любое время, есть статическая структура, которая не допускает добавления после создания. Это экономит накладные расходы на один дикт для каждого объекта, который использует слоты
Вторая половина - это желаемое за действительное и нестандартное мышление:
Хотя иногда это полезная оптимизация, она была бы совершенно ненужной, если бы интерпретатор Python был достаточно динамичным, так что он требовал бы диктовку только тогда, когда фактически были дополнения к объекту.
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__
, (The__dict__
не инициализируется до тех пор, пока вы его не используете, поэтому вам не нужно беспокоиться о пространстве, занимаемом пустым словарем для каждого создаваемого вами экземпляра.) Если вам не нужно это дополнительное пространство, вы можете добавить фразу "__slots__ = []
"к вашему классу.
Цитируя Джейкоба Халлена:
Правильное использование
__slots__
это сэкономить место в объектах. Вместо того, чтобы иметь динамический диктант, который позволяет добавлять атрибуты к объектам в любое время, есть статическая структура, которая не допускает добавления после создания. [Это использование__slots__
исключает накладные расходы в размере одного dict для каждого объекта.] Хотя это иногда является полезной оптимизацией, было бы совершенно ненужным, если бы интерпретатор Python был достаточно динамичным, чтобы он требовал только dict, когда на самом деле были дополнения к объекту.К сожалению, у слотов есть побочный эффект. Они изменяют поведение объектов, имеющих слоты, таким образом, что они могут быть использованы злоупотребляющими средствами управления и статичными печатями. Это плохо, потому что уродцы управления должны злоупотреблять метаклассами, а статические типизаторы должны злоупотреблять декораторами, поскольку в Python должен быть только один очевидный способ сделать что-то.
Делаем CPython достаточно умным, чтобы экономить место без
__slots__
является важным мероприятием, поэтому, вероятно, его нет в списке изменений для P3k (пока).
Вы хотели бы использовать __slots__
если вы собираетесь создать множество (сотни, тысячи) объектов одного и того же класса. __slots__
существует только как инструмент оптимизации памяти.
Настоятельно не рекомендуется использовать __slots__
для ограничения создания атрибутов, и в целом вы хотите избежать этого, потому что это нарушает pickle, наряду с некоторыми другими функциями самоанализа python.
Каждый объект Python имеет __dict__
атрибут, который является словарем, содержащим все другие атрибуты. например, когда вы печатаете self.attr
Python на самом деле делает self.__dict__['attr']
, Как вы можете себе представить, использование словаря для хранения атрибута требует дополнительного пространства и времени для доступа к нему.
Тем не менее, когда вы используете __slots__
любой объект, созданный для этого класса, не будет иметь __dict__
приписывать. Вместо этого весь доступ к атрибутам осуществляется напрямую через указатели.
Поэтому, если вам нужна структура стиля C, а не полноценный класс, вы можете использовать __slots__
для уменьшения размеров объектов и сокращения времени доступа к атрибутам. Хорошим примером является класс Point, содержащий атрибуты x & y. Если у вас будет много очков, вы можете попробовать использовать __slots__
чтобы сохранить память.
В дополнение к другим ответам, вот пример использования __slots__
:
>>> class Test(object): #Must be new-style class!
... __slots__ = ['x', 'y']
...
>>> pt = Test()
>>> dir(pt)
['__class__', '__delattr__', '__doc__', '__getattribute__', '__hash__',
'__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__slots__', '__str__', 'x', 'y']
>>> pt.x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: x
>>> pt.x = 1
>>> pt.x
1
>>> pt.z = 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute 'z'
>>> pt.__dict__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute '__dict__'
>>> pt.__slots__
['x', 'y']
Итак, реализовать __slots__
, он занимает только дополнительную строку (и делает ваш класс классом нового стиля, если это еще не сделано). Таким образом, вы можете уменьшить объем памяти этих классов в 5 раз, за счет необходимости написания собственного кода, если и когда это станет необходимым.
Слоты очень полезны для библиотечных вызовов, чтобы исключить "диспетчеризацию именованных методов" при вызовах функций. Это упоминается в документации SWIG. Для высокопроизводительных библиотек, которые хотят сократить накладные расходы на функции для часто вызываемых функций, используя слоты намного быстрее.
Теперь это не может быть напрямую связано с вопросом ОП. Это больше относится к созданию расширений, чем к использованию синтаксиса слотов в объекте. Но это помогает завершить картину использования слотов и некоторые причины, стоящие за ними.
Очень простой пример __slot__
приписывать.
Проблема: без __slots__
Если у меня нет __slot__
атрибут в моем классе, я могу добавить новые атрибуты для моих объектов.
class Test:
pass
obj1=Test()
obj2=Test()
print(obj1.__dict__) #--> {}
obj1.x=12
print(obj1.__dict__) # --> {'x': 12}
obj1.y=20
print(obj1.__dict__) # --> {'x': 12, 'y': 20}
obj2.x=99
print(obj2.__dict__) # --> {'x': 99}
Если вы посмотрите на пример выше, вы увидите, что obj1 и obj2 имеют свои собственные атрибуты x и y, а python также создал dict
атрибут для каждого объекта (obj1 и obj2).
Предположим, есть ли в моем классе Test тысячи таких объектов? Создание дополнительного атрибута dict
для каждого объекта будет много накладных расходов (память, вычислительная мощность и т. д.) в моем коде.
Решение: с __slots__
Теперь в следующем примере мой класс Test содержит __slots__
приписывать. Теперь я не могу добавить новые атрибуты в мои объекты (кроме атрибута x
) и Python не создает dict
атрибут больше. Это устраняет накладные расходы для каждого объекта, которые могут стать значительными, если у вас много объектов.
class Test:
__slots__=("x")
obj1=Test()
obj2=Test()
obj1.x=12
print(obj1.x) # --> 12
obj2.x=99
print(obj2.x) # --> 99
obj1.y=28
print(obj1.y) # --> AttributeError: 'Test' object has no attribute 'y'
Атрибут экземпляра класса имеет 3 свойства: экземпляр, имя атрибута и значение атрибута.
При обычном доступе к атрибуту экземпляр действует как словарь, а имя атрибута выступает в качестве ключа в этом словаре при поиске значения.
экземпляр (атрибут) -> значение
При доступе __slots__ имя атрибута действует как словарь, а экземпляр выступает в качестве ключа в словаре при поиске значения.
атрибут (экземпляр) -> значение
В шаблоне flyweight имя атрибута действует как словарь, а значение выступает в качестве ключа в этом словаре при поиске экземпляра.
атрибут (значение) -> экземпляр
В дополнение к другим ответам также добавляет немного типографской безопасности, ограничивая атрибуты предопределенным списком. Это уже давно является проблемой JavaScript, который также позволяет вам добавлять новые атрибуты к существующему объекту, хотели вы этого или нет.
Вот обычный объект без слотов, который ничего не делает, но позволяет добавлять атрибуты:
class Unslotted:
pass
test = Unslotted()
test.name = 'Fred'
test.Name = 'Wilma'
Поскольку Python чувствителен к регистру, два атрибута, написанные одинаково, но с разным регистром, различны. Если вы подозреваете, что одна из них является опечаткой, то вам не повезло.
Используя слоты, вы можете ограничить это:
class Slotted:
__slots__ = ('name')
pass
test = Slotted()
test.name = 'Fred' # OK
test.Name = 'Wilma' # Error
На этот раз второй атрибут (
Name
) запрещен, так как его нет в коллекции.
Я бы предположил, что, вероятно, лучше использовать
__slots__
где возможно сохранить больший контроль над объектом.
Начиная с Python 3.9,
dict
может использоваться для добавления описаний к атрибутам через
__slots__
.
None
могут использоваться для атрибутов без описаний, а частные переменные не будут отображаться, даже если дано описание.
class Person:
__slots__ = {
"birthday":
"A datetime.date object representing the person's birthday.",
"name":
"The first and last name.",
"public_variable":
None,
"_private_variable":
"Description",
}
help(Person)
"""
Help on class Person in module __main__:
class Person(builtins.object)
| Data descriptors defined here:
|
| birthday
| A datetime.date object representing the person's birthday.
|
| name
| The first and last name.
|
| public_variable
"""
Еще одно несколько неясное использование __slots__
заключается в добавлении атрибутов к объектному прокси из пакета ProxyTypes, который ранее был частью проекта PEAK. это ObjectWrapper
позволяет вам прокси-объект другого объекта, но перехватывать все взаимодействия с прокси-объектом. Он не очень широко используется (и не поддерживает Python 3), но мы использовали его для реализации поточно-ориентированной блокирующей оболочки вокруг асинхронной реализации, основанной на торнадо, которая перенаправляет весь доступ к проксируемому объекту через ioloop, используя потокобезопасный concurrent.Future
объекты для синхронизации и возврата результатов.
По умолчанию любой атрибут доступа к прокси-объекту даст вам результат от прокси-объекта. Если вам нужно добавить атрибут на объект прокси, __slots__
может быть использован.
from peak.util.proxies import ObjectWrapper
class Original(object):
def __init__(self):
self.name = 'The Original'
class ProxyOriginal(ObjectWrapper):
__slots__ = ['proxy_name']
def __init__(self, subject, proxy_name):
# proxy_info attributed added directly to the
# Original instance, not the ProxyOriginal instance
self.proxy_info = 'You are proxied by {}'.format(proxy_name)
# proxy_name added to ProxyOriginal instance, since it is
# defined in __slots__
self.proxy_name = proxy_name
super(ProxyOriginal, self).__init__(subject)
if __name__ == "__main__":
original = Original()
proxy = ProxyOriginal(original, 'Proxy Overlord')
# Both statements print "The Original"
print "original.name: ", original.name
print "proxy.name: ", proxy.name
# Both statements below print
# "You are proxied by Proxy Overlord", since the ProxyOriginal
# __init__ sets it to the original object
print "original.proxy_info: ", original.proxy_info
print "proxy.proxy_info: ", proxy.proxy_info
# prints "Proxy Overlord"
print "proxy.proxy_name: ", proxy.proxy_name
# Raises AttributeError since proxy_name is only set on
# the proxy object
print "original.proxy_name: ", proxy.proxy_name
В дополнение к множеству преимуществ, описанных в других ответах здесь - компактные экземпляры для памяти, менее подверженные ошибкам, чем более изменчивые__dict__
-несущие экземпляры и т. д . Я считаю, что использование предлагает более четкие объявления классов, поскольку переменные экземпляра класса явно находятся в открытом доступе.
Чтобы бороться с проблемами наследования с объявлениями, я использую этот метакласс:
import abc
class Slotted(abc.ABCMeta):
""" A metaclass that ensures its classes, and all subclasses,
will be slotted types.
"""
def __new__(metacls, name, bases, attributes, **kwargs):
""" Override for `abc.ABCMeta.__new__(…)` setting up a
derived slotted class.
"""
if '__slots__' not in attributes:
attributes['__slots__'] = tuple()
return super(Slotted, metacls).__new__(metacls, name, # type: ignore
bases,
attributes,
**kwargs)
… который, если он объявлен как метакласс базового класса в башне наследования, гарантирует, что все, что происходит от этого базового класса, будет должным образом наследоваться__slots__
атрибуты, даже если промежуточный класс не может их объявить. Вот так:
# note no __slots__ declaration necessary with the metaclass:
class Base(metaclass=Slotted):
pass
# class is properly slotted, no __dict__:
class Derived(Base):
__slots__ = 'slot', 'another_slot'
# class is also properly slotted:
class FurtherDerived(Derived):
pass
Первоначальный вопрос был о случаях общего использования, а не только о памяти. Таким образом, здесь следует упомянуть, что вы также получаете лучшую производительность при создании большого количества объектов - это интересно, например, при разборе больших документов на объекты или из базы данных.
Вот сравнение создания деревьев объектов с миллионами записей, используя слоты и без слотов. В качестве ссылки также производительность при использовании простых диктов для деревьев (Py2.7.10 в OSX):
********** RUN 1 **********
1.96036410332 <class 'css_tree_select.element.Element'>
3.02922606468 <class 'css_tree_select.element.ElementNoSlots'>
2.90828204155 dict
********** RUN 2 **********
1.77050495148 <class 'css_tree_select.element.Element'>
3.10655999184 <class 'css_tree_select.element.ElementNoSlots'>
2.84120798111 dict
********** RUN 3 **********
1.84069895744 <class 'css_tree_select.element.Element'>
3.21540498734 <class 'css_tree_select.element.ElementNoSlots'>
2.59615707397 dict
********** RUN 4 **********
1.75041103363 <class 'css_tree_select.element.Element'>
3.17366290092 <class 'css_tree_select.element.ElementNoSlots'>
2.70941114426 dict
Тестовые классы (идент, аппарт из слотов):
class Element(object):
__slots__ = ['_typ', 'id', 'parent', 'childs']
def __init__(self, typ, id, parent=None):
self._typ = typ
self.id = id
self.childs = []
if parent:
self.parent = parent
parent.childs.append(self)
class ElementNoSlots(object): (same, w/o slots)
Тестовый код, подробный режим:
na, nb, nc = 100, 100, 100
for i in (1, 2, 3, 4):
print '*' * 10, 'RUN', i, '*' * 10
# tree with slot and no slot:
for cls in Element, ElementNoSlots:
t1 = time.time()
root = cls('root', 'root')
for i in xrange(na):
ela = cls(typ='a', id=i, parent=root)
for j in xrange(nb):
elb = cls(typ='b', id=(i, j), parent=ela)
for k in xrange(nc):
elc = cls(typ='c', id=(i, j, k), parent=elb)
to = time.time() - t1
print to, cls
del root
# ref: tree with dicts only:
t1 = time.time()
droot = {'childs': []}
for i in xrange(na):
ela = {'typ': 'a', id: i, 'childs': []}
droot['childs'].append(ela)
for j in xrange(nb):
elb = {'typ': 'b', id: (i, j), 'childs': []}
ela['childs'].append(elb)
for k in xrange(nc):
elc = {'typ': 'c', id: (i, j, k), 'childs': []}
elb['childs'].append(elc)
td = time.time() - t1
print td, 'dict'
del droot
Вы - по существу - бесполезны для __slots__
,
В то время, когда вы думаете, что вам может понадобиться __slots__
, вы на самом деле хотите использовать шаблоны Lightweight или Flyweight. Это случаи, когда вы больше не хотите использовать чисто объекты Python. Вместо этого вам нужна объектно-подобная обертка Python для массива, структуры или массива numpy.
class Flyweight(object):
def get(self, theData, index):
return theData[index]
def set(self, theData, index, value):
theData[index]= value
Классоподобная оболочка не имеет атрибутов - она просто предоставляет методы, которые воздействуют на базовые данные. Методы могут быть сведены к методам класса. На самом деле, это может быть сведено к функциям, работающим с базовым массивом данных