Сообщить об ошибке.

Специальный атрибут __slots__ класса Python

Специальный атрибут __slots__ позволяют явно объявлять элементы данных (например, свойства) и запрещать создание словаря __dict__ и __weakref__ (если явно не объявлено в __slots__ или не доступно в родительском элементе).

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

Наличие магического атрибута __slots__ делает несколько вещей. Во-первых, он ограничивает допустимый набор имен атрибутов объекта только перечисленными именами. Во-вторых, поскольку атрибуты теперь фиксированы, больше нет необходимости хранить атрибуты в словаре экземпляра, поэтому атрибут __dict__ удаляется (если только базовый класс уже не имеет его; он также может быть добавлен обратно подклассом, который не имеет __slots__). При использовании атрибута __slots__ атрибуты хранятся в заранее определенных местах в массиве. Таким образом, каждый атрибут слота на самом деле является объектом дескриптора, который знает, как установить/получить каждый атрибут с помощью индекса массива. Реализация этой функции полностью на языке C и очень эффективна.

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

  • При наследовании от класса без атрибута __slots__ атрибуты __dict__ и __weakref__ экземпляров всегда будут доступны.
  • Без переменной словаря __dict__, экземплярам нельзя назначить новые переменные/атрибуты, не указанные в определении __slots__. При попытке присвоения имени переменной, не указанной в списке, возникает ошибка AttributeError. Если требуется динамическое присвоение новых переменных, добавьте значение '__dict__' к последовательности строк в объявлении атрибута __slots__.
  • Без переменной __weakref__ для каждого экземпляра, классы, определяющие __slots__, не поддерживают слабые ссылки на его экземпляры. Если требуется поддержка слабых ссылок, добавьте значение '__weakref__' к последовательности строк в объявлении атрибута __slots__.
  • Магический атрибут __slots__ реализуются на уровне класса путем создания дескрипторов для каждого имени переменной. В результате атрибуты класса не могут использоваться для установки значений по умолчанию для переменных экземпляра, определенных в методе __slots__, в противном случае атрибут класса перезапишет назначение дескриптора.
  • Действие атрибута __slots__ не ограничивается классом, в котором он определен. Атрибуты __slots__, объявленные в родительских классах, доступны в дочерних классах. Однако дочерние подклассы получат __dict__ и __weakref__, если они не определят __slots__ (который должен содержать только имена любых дополнительных слотов).
  • Если класс определяет слот, также определенный в базовом классе, переменная экземпляра, определенная слотом базового класса, недоступна (кроме как путем получения ее дескриптора непосредственно из базового класса). Это делает поведение программы неопределенным. В будущем может быть добавлена ​​проверка для предотвращения этого.
  • Непустой атрибут __slots__ не работает для классов, производных, от встроенных типов переменной длины, таких как int, bytes и tuple.
  • Атрибуту __slots__ может быть назначен любой нестроковый итерируемый объект. Также могут использоваться словари, при этом в будущем, значениям, соответствующим каждому ключу, может быть присвоено особое значение.
  • Назначение __class__ работает, только если оба класса имеют одинаковые __slots__.
  • Может использоваться множественное наследование с несколькими родительскими классами с разделением на слоты, но только одному родительскому элементу разрешено иметь атрибуты, созданные с помощью слотов (другие классы должны иметь макеты пустых слотов) - нарушения вызывают исключение TypeError.
  • Если для __slots__ используется итератор, то для каждого значения итератора будет создаваться дескриптор, при этом атрибут __slots__ будет пустым итератором.

Пример использования атрибута __slots__.

class Slot:
    __slots__ = ["a", "b"]

>>> slot = Slot()
>>> slot.a = 1
>>> slot.a
# 1
>>> slot.a = 130
>>> slot.a
# 130
>>> slot.b = 13
>>> slot.b
# 13

# атрибут `c` не определен в списке `__slots__`
>>> slot.c = 1
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# AttributeError: 'Slot' object has no attribute 'c'
>>> slot.__dict__
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# AttributeError: 'Slot' object has no attribute '__dict__'

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

class Test(object):
    __slots__ = ('a', 'b', '__dict__')

>>> t = Test
# переменные `foo` и `baz` не 
# определены в `__slots__`
>>> t.foo, t.baz = 10, 100
>>> t.foo, t.baz
# (10, 100)

Если в классе будет использоваться функциональность слабых ссылок на объекты, то ее так же можно добавить добавив значение '__weakref__' в кортеж атрибута __slots__.

При использовании __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.0844320549999793
>>> min(timeit.repeat(get_set_delete_fn(not_slotted)))
0.10562241299999187

Использование __slots__ при множественном наследовании.

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

class Foo(object): 
    __slots__ =  'foo', 'bar'

class Bar(object):
    __slots__ = 'foo', 'bar'

# будет работать если в одном из 
# родительских классов `__slots__ = ()`
>>> class Baz(Foo, Bar): pass
... 
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: multiple bases have instance lay-out conflict

Использование пустого __slots__ в родительском классе, обеспечивает наибольшую гибкость, позволяя дочерним классам выбирать, предотвращать или разрешать создание __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__ с определенными атрибутами в родительских классах и если их добавить позже в дочернем классе, то это не должно вызвать никаких проблем.

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

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

class AbstractBase:
    __slots__ = ()
    def __init__(self, a, b):
        self.a = a
        self.b = b

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

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

Теперь, если у обоих базовых классов были слоты со своими атрибутами, то невозможно было бы сделать следующее.

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

Фактически, можно было в AbstractBase оставить __slots__ = 'a', 'b' и исключить их из объявления класса TestChild(), но оставлять их было бы неправильно. Зато теперь класс TestChild() имеет функциональность от обоих родительских классов через множественное наследование, и по-прежнему можно запретить создание экземпляров __dict__ и __weakref__:

>>> test = TestChild('a', 'b')
>>> test.c = 'c'
# setting c!
>>> test.c
# getting c!
# 'c'

# атрибут не определен
>>> test.d = 'd'
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# AttributeError: 'TestChild' object has no attribute 'd'