Какова цель __slots__ в Python?
Какова цель __slots__ в Python. Особенно в отношении того, когда его использовать, а когда нет?
перевод вопроса Usage of __slots__? от участника @Jeb
Ответы (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' # избыточные foo и bar
Python не возражает, когда вы ошибаетесь (вероятно должен), иначе проблемы могут не проявиться, но ваши объекты будут занимать больше места, чем должны.
Python 3.8:
>>> from sys import getsizeof
>>> getsizeof(Right()), getsizeof(Wrong())
(56, 72)
Это происходит потому, что дескриптор слота Base имеет слот, отдельный от Wrong.
>>> 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.Чтобы предотвратить создание
__dict__, вы должны наследовать отobjectw, и все классы в наследовании должны объявлять__slots__, и ни один из них не может иметь запись__dict__.
Если вы хотите продолжить чтение, есть ещё много деталей.
Зачем использовать __slots__: более быстрый доступ к атрибутам.
Создатель Python, Guido van Rossum, утверждает, что на самом деле он создал __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
В Python 3.5 на Ubuntu доступ с использованием слотов почти на 30% быстрее.
>>> 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__* | *(слоты не определены)
none 16 56 + 272† 16 56 + 112† | †если __dict__ упоминается
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
Итак, несмотря на меньшие __dict__ в 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__, вы должны создать подкласс объекта:
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() # Нет ошибок
Добавьте '__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' # увы, будет работать, если пусто, т.е. ()
>>> 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'
Но нас это не волнует, это тривиальное одиночное наследование, нам нужен другой класс, от которого мы также могли бы унаследовать, возможно, с шумным атрибутом:
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), в которую я недавно внес значительный вклад.
перевод ответа от участника @AaronHall