аватар question@mail.ru · 01.01.1970 03:00

Какова цель __slots__ в Python?

Какова цель __slots__ в Python. Особенно в отношении того, когда его использовать, а когда нет?

перевод вопроса от участника

аватар answer@mail.ru · 01.01.1970 03:00
n

Какова цель __slots__ в Python. Особенно в отношении того, когда его использовать, а когда нет?

n
n

TLDR:

n

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

n
    n
  1. быстрый доступ к атрибутам.
  2. n
  3. экономия памяти.
  4. n
n

Место экономится потому что

n
    n
  1. n

    Ссылки сохраняются на значение слотов а не в __dict__

    n
  2. n
  3. n

    Запрещение создания __dict__ и __weakref__, если родительские классы запрещают их, и вы объявляете __slots__.

    n
  4. n
n

Краткие предупреждения

n

В дереве наследования объявлять определённый слот надо лишь один раз

n

Например:

n
class Base:n    __slots__ = 'foo', 'bar'nclass Right(Base):n    __slots__ = 'baz',n    nclass Wrong(Base):n    __slots__ = 'foo', 'bar', 'baz' # избыточные foo и barn
n

Python не возражает, когда вы ошибаетесь (вероятно должен), иначе проблемы могут не проявиться, но ваши объекты будут занимать больше места, чем должны.

n

Python 3.8:

n
>>> from sys import getsizeofn>>> getsizeof(Right()), getsizeof(Wrong())n(56, 72) n
n

Это происходит потому, что дескриптор слота Base имеет слот, отдельный от Wrong.

n
>>> w = Wrong()n>>> w.foo = 'foo'n>>> Base.foo.__get__(w)nTraceback (most recent call last):n  File ""<stdin>"", line 1, in <module>nAttributeError: foon>>> Wrong.foo.__get__(w)n'foo'n
n

Самое большое предостережение касается множественного наследования - несколько «родительских классов с непустыми слотами» не могут быть объединены.

n

Чтобы обойти это ограничение, следуйте рекомендациям: Вычлените все абстракции родителей, кроме одного или всех, от которых будет унаследован их конкретный класс, и ваш новый класс - предоставив абстракциям пустые слоты(точно так же, как абстрактные базовые классы в стандартной библиотеке)

n

См. Пример ниже в разделе о множественном наследовании.

n

Требования:

n
    n
  1. n

    Чтобы атрибуты, названные в __slots__, хранились в слотах а не в __dict__, класс должен наследовать от object.

    n
  2. n
  3. n

    Чтобы предотвратить создание __dict__, вы должны наследовать от objectw, и все классы в наследовании должны объявлять __slots__, и ни один из них не может иметь запись __dict__.

    n
  4. n
n

Если вы хотите продолжить чтение, есть ещё много деталей.

n

Зачем использовать __slots__: более быстрый доступ к атрибутам.

n

Создатель Python, Guido van Rossum, , что на самом деле он создал __slots__ для более быстрого доступа к атрибутам.

n

Продемонстрировать боле�� быстрый доступ - это тривиальная задача:

n
import timeitnclass Foo(object): __slots__ = 'foo',nclass Bar(object): passnslotted = Foo()nnot_slotted = Bar()ndef get_set_delete_fn(obj):n    def get_set_delete():n        obj.foo = 'foo'n        obj.foon        del obj.foon    retu get_set_deleten
n

и

n
>>> min(timeit.repeat(get_set_delete_fn(slotted)))n0.2846834529991611n>>> min(timeit.repeat(get_set_delete_fn(not_slotted)))n0.3664822799983085 n
n

В Python 3.5 на Ubuntu доступ с использованием слотов почти на 30% быстрее.

n
>>> 0.3664822799983085 / 0.2846834529991611n1.2873325658284342 n
n

Я измерил это В Python 2 в Windows получилось что примерно на 15% быстрее.

n

Зачем использовать __slots__: экономия памяти

n

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

n

n
n

Пространство, сэкономленное при использовании __dict__, может быть значительным.

n
n

значительную экономию памяти приписывает __slots__.

n

Чтобы проверить это, используя дистрибутив Anaconda для Python 2.7 в Ubuntu Linux с guppy.hpy (он же heapy) и sys.getsizeof, размер экземпляра класса без объявленного __slots__ и ничего другого составляет 64 байта. Это не включает __dict__. Еще раз спасибо Python за ленивую оценку, __dict__, по-видимому, не вызывается до тех пор, пока на него не будет ссылаться, но классы без данных обычно бесполезны. При вызове атрибут __dict__ имеет дополнительно минимум 280 байтов.

n

Напротив, экземпляр класса с __slots__, объявленным как () (без данных), составляет всего 16 байтов, и всего 56 байтов с одним элементом в слотах, 64 с двумя.

n

Для 64-битного Python я проиллюстрирую потребление памяти в байтах в Python 2.7 и 3.6 для __slots__ и __dict__ (слоты не определены) для каждой точки, где dict увеличивается в 3.6 (кроме атрибутов 0, 1 и 2):

n
           Python 2.7             Python 3.6n    attrs  __slots__  __dict__*   __slots__  __dict__* | *(слоты не определены)n    none   16         56 + 272†   16         56 + 112† | †если __dict__ упоминаетсяn    one    48         56 + 272    48         56 + 112n    two    56         56 + 272    56         56 + 112n    six    88         56 + 1040   88         56 + 152n    11     128        56 + 1040   128        56 + 240n    22     216        56 + 3344   216        56 + 408     n    43     384        56 + 3344   384        56 + 752n
n

Итак, несмотря на меньшие __dict__ в Python 3, мы видим, насколько хорошо __slots__ масштабируется для экземпляров, чтобы сэкономить нам память, и это основная причина, по которой нужно использовать __slots__.

n

Просто для полноты моих заметок обратите внимание, что существует единовременная стоимость одного слота в пространстве имен класса в 64 байта в Python 2 и 72 байта в Python 3, потому что слоты используют дескрипторы данных, такие как свойства, называемые «членами».

n
>>> Foo.foon<member 'foo' of 'Foo' objects>n>>> type(Foo.foo)n<class 'member_descriptor'>n>>> getsizeof(Foo.foo)n72n
n

Демонстрация __slots__: Чтобы запретить создание __dict__, вы должны создать подкласс объекта:

n
class Base(object):n     __slots__ = ()n
n

теперь же

n
>>> b = Base()n>>> b.a = 'a'nTraceback (most recent call last):n  File ""<pyshell#38>"", line 1, in <module>n    b.a = 'a'nAttributeError: 'Base' object has no attribute 'a'n
n

Или создайте подкласс другого класса, который определяет __slots__

n
class Child(Base):n    __slots__ = ('a',)n
n

И теперь:

n
c = Child()nc.a = 'a'n
n

но:

n
>>> c.b = 'b'nTraceback (most recent call last):n  File ""<pyshell#42>"", line 1, in <module>n    c.b = 'b'nAttributeError: 'Child' object has no attribute 'b'n
n

Чтобы разрешить создание __dict__ при создании подкласса для объектов со слотами, просто добавьте __dict__ к __slots__ (обратите внимание, что слоты упорядочены, и вы не должны повторять слоты, которые уже находятся в родительских классах):

n
class SlottedWithDict(Child): n    __slots__ = ('__dict__', 'b')nswd = SlottedWithDict()nswd.a = 'a'nswd.b = 'b'nswd.c = 'c'n
n

и

n
>>> swd.__dict__n{'c': 'c'}n
n

Или вам даже не нужно объявлять __slots__ в своем подклассе, и вы по-прежнему будете использовать слоты от родителей, но не ограничивать создание __dict__:

n
class NoSlots(Child): passnns = NoSlots()nns.a = 'a'nns.b = 'b'n
n

и:

n
>>> ns.__dict__n{'b': 'b'}n
n

Однако __slots__ может вызвать проблемы при множественном наследовании.

n
class BaseA(object): n    __slots__ = ('a',)nclass BaseB(object): n    __slots__ = ('b',) n
n

Создание дочернего класса от родителей с обоими непустыми слотами не удается:

n
>>> class Child(BaseA, BaseB): __slots__ = ()nTraceback (most recent call last):n  File ""<pyshell#68>"", line 1, in <module>n    class Child(BaseA, BaseB): __slots__ = ()nTypeError: Error when calling the metaclass basesn    multiple bases have instance lay-out conflict n
n

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

n
from abc import ABCnclass AbstractA(ABC):n    __slots__ = ()nclass BaseA(AbstractA): n    __slots__ = ('a',)nclass AbstractB(ABC):n    __slots__ = ()nclass BaseB(AbstractB): n    __slots__ = ('b',)nclass Child(AbstractA, AbstractB): n    __slots__ = ('a', 'b')nc = Child() # Нет ошибокn
n

Добавьте '__dict__' к __slots__ чтобы получить динамическое назначение:

n
class Foo(object):n    __slots__ = 'bar', 'baz', '__dict__'n
n

и сейчас

n
>>> foo = Foo()n>>> foo.boink = 'boink'n
n

Таким образом, с '__dict__' в слотах мы теряем некоторые преимущества размера с преимуществом наличия динамического назначения и по-прежнему наличия слотов для имен, которые мы ожидаем

n

Когда вы наследуете объект, который не имеет слотов, вы получаете такую же семантику, когда используете __slots__ - имена, которые находятся в __slots__, указывают на значения, размещенные в слотах, тогда как любые другие значения помещаются в __dict__ экземпляра.

n

Избегать __slots__, потому что вы хотите иметь возможность добавлять атрибуты на лету, на самом деле не является хорошей причиной - просто добавьте '__dict__' в свой __slots__, если это необходимо.

n

Вы можете точно так же явно добавить __weakref__ в __slots__, если вам нужна эта функция.

n

Установите пустой кортеж при создании подкласса именованного кортежа:

n

Встроенная класс namedtuple создает очень легкие неизменяемые экземпляры (по сути, размер кортежей), но чтобы получить преимущества, вам нужно сделать это самостоятельно, если вы подклассифицируете их:

n
from collections import namedtuplenclass MyNT(namedtuple('MyNT', 'bar baz')):n    """"""MyNT is an immutable and lightweight object""""""n    __slots__ = () n
n

использование

n
>>> nt = MyNT('bar', 'baz')n>>> nt.ba'bar'n>>> nt.bazn'baz'n
n

А попытка назначить неожиданный атрибут вызывает ошибку AttributeError, потому что мы предотвратили создание __dict__:

n
>>> nt.quux = 'quux'nTraceback (most recent call last):n  File ""<stdin>"", line 1, in <module>nAttributeError: 'MyNT' object has no attribute 'quux'n
n

Вы можете разрешить создание __dict__, отключив __slots__ = (), но вы не можете использовать непустые __slots__ с подтипами кортежа.

n

Важнейшее предостережение: множественное наследование

n

Даже если непустые слоты одинаковы для нескольких родителей, их нельзя использовать вместе:

n
class Foo(object): n    __slots__ = 'foo', 'bar'nclass Bar(object):n    __slots__ = 'foo', 'bar' # увы, будет работать, если пусто, т.е. ()n>>> class Baz(Foo, Bar): passnTraceback (most recent call last):n  File ""<stdin>"", line 1, in <module>nTypeError: Error when calling the metaclass basesn    multiple bases have instance lay-out conflict n
n

Использование пустого __slots__ в родительском элементе, ��о-видимому, обеспечивает максимальную гибкость, позволяя дочернему элементу выбирать, предотвращать или разрешать (добавляя '__dict__' для получения динамического назначения, см. Раздел выше) создание __dict__:

n
class Foo(object): __slots__ = ()nclass Bar(object): __slots__ = ()nclass Baz(Foo, Bar): __slots__ = ('foo', 'bar')nb = Baz()nb.foo, b.bar = 'foo', 'bar'n
n

Вам не обязательно иметь слоты - поэтому, если вы добавите их и удалите позже, это не должно вызвать никаких проблем.

n

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

n

Чтобы продемонстрировать, сначала давайте создадим код с классом, который мы хотели бы использовать при множественном наследовании.

n
class AbstractBase:n    __slots__ = ()n    def __init__(self, a, b):n        self.a = an        self.b = bn    def __repr__(self):n        retu f'{type(self).__name__}({repr(self.a)}, {repr(self.b)})'n
n

Мы могли бы использовать вышесказанное непосредственно путем наследования и объявления ожидаемых слотов:

n
class Foo(AbstractBase):n    __slots__ = 'a', 'b' n
n

Но нас это не волнует, это тривиальное одиночное наследование, нам нужен другой класс, от которого мы также могли бы унаследовать, возможно, с шумным атрибутом:

n
class AbstractBaseC:n    __slots__ = ()n    @propertyn    def c(self):n        print('getting c!')n        retu self._cn    @c.sette    def c(self, arg):n        print('setting c!')n        self._c = argn
n

Теперь, если бы на обеих базах были непустые слоты, мы не смогли бы сделать следующее. (На самом деле, если бы мы хотели, мы могли бы дать AbstractBase непустые слоты a и b и исключить их из приведенного ниже объявления - оставлять их было бы неправильно):

n
class Concretion(AbstractBase, AbstractBaseC):n    __slots__ = 'a b _c'.split() n
n

И теперь у нас есть функциональность от обоих через множественное наследование, и мы все еще можем запретить создание экземпляров __dict__ и __weakref__:

n
>>> c = Concretion('a', 'b')n>>> c.c = cnsetting c!n>>> c.cngetting c!nConcretion('a', 'b')n>>> c.d = 'd'nTraceback (most recent call last):n  File ""<stdin>"", line 1, in <module>nAttributeError: 'Concretion' object has no attribute 'd'n
n

Другие случаи, чтобы избежать слотов:

n
    n
  • n

    Избегайте их, если вы хотите выполнить назначение __class__ с другим классом, у которого их нет (и вы не можете их добавить), если макеты слотов не идентичны. (Мне очень интересно узнать, кто это делает и почему.)

    n
  • n
  • n

    Избегайте их, если вы хотите создать подкласс встроенных функций переменной длины, таких как long, tuple или str, и хотите добавить к ним атрибуты.

    n
  • n
  • n

    Избегайте их, если вы настаиваете на предоставлении значений по умолчанию через атрибуты класса для переменных экземпляра.

    n
  • n
n

Возможно, вам удастся выявить дополнительные предостережения из остальной части , в которую я недавно внес значительный вклад.

n

перевод от участника

Последние

Похожие