Використання __slots__?


Відповіді:


1018

У Python, яка мета __slots__і в яких випадках слід цього уникати?

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)

Це пояснюється тим, що дескриптор слотів Base має слот, окремий від неправильного. Зазвичай це не може з'являтися, але це може:

>>> 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__, ви повинні перейти в спадщину від, 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__.

Щоб переконатися в цьому, використовуючи розподіл Анаконди в 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:

Вбудований nametuple зробить незмінні екземпляри, які дуже легкі (по суті, розмір кортежів), але щоб отримати переваги, потрібно зробити це самостійно, якщо ви підкласируєте їх:

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'

Але нам це не байдуже, це тривіальне одинакове успадкування, нам потрібен інший клас, від якого ми можемо також успадкувати, можливо, із шумним атрибутом:

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, кортеж або 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__. ( __dict__Ініціалізація не ініціалізована, поки ви не використовуєте її, тому вам не слід турбуватися про простір, який займає порожній словник для кожного створеного вами екземпляра.) Якщо вам не потрібно це додаткове місце, ви можете додати фразу " __slots__ = []" в ваш клас.


14
вау, одна пекло відповіді - спасибі! Однак я не нагадав class Child(BaseA, BaseB): __slots__ = ('a', 'b')приклад із батьками-еміті-слотами. Чому тут dictproxyстворено замість піднявши AttributeErrorдля c?
Skandix

@Skandix спасибі за те, що я звернув цю помилку на друк, виявилося, що це не була інформація, я, мабуть, забув, що я редагував цю частину, коли я зберег її в історію публікацій. Це, мабуть, було б спіймано раніше, якби я зробив правильну справу і зробив код більш копіюваним для копіювання ... Ще раз дякую!
Аарон Холл

38
Ця відповідь має бути частиною офіційної документації Python о __slots__. Серйозно! Дякую!
NightElfik

13
@NightElfik віриш чи ні, я взяв внесок у документи Python __slots__приблизно рік тому: github.com/python/cpython/pull/1819/files
Aaron Hall

Фантастично детальна відповідь. У мене є одне питання: чи варто використовувати слоти за замовчуванням, якщо використання не потрапляє в один із застережень, або якщо слоти щось розглядають, якщо ви знаєте, що збираєтеся боротися за швидкість / пам'ять? Якщо говорити іншим способом, чи варто спонукати новачка дізнаватися про них та використовувати їх із самого початку?
freethebees

265

Цитуючи Якова Халлена :

Правильне використання - __slots__це економія місця в об'єктах. Замість того, щоб мати динамічний дікт, який дозволяє додавати атрибути до об’єктів у будь-який час, існує статична структура, яка не дозволяє додавати після створення. [Це використання __slots__виключає накладні витрати одного диктату для кожного об'єкта.] Хоча це іноді корисна оптимізація, було б абсолютно непотрібно, якби інтерпретатор Python був досить динамічним, щоб вимагати дікта лише тоді, коли насправді були доповнення до об’єкт.

На жаль, є побічний ефект для слотів. Вони змінюють поведінку об'єктів, які мають слоти, таким чином, щоб їх можна було зловживати керуючими виродками та статичним набором вихідних. Це погано, тому що контрольні виродки повинні зловживати метакласами, а статичні друкарські вихідні повинні зловживати декораторами, оскільки в Python має бути лише один очевидний спосіб робити щось.

Зробити CPython достатньо розумним для обробки економії місця без __slots__- це головне завдання, ймовірно, тому він ще не в списку змін для P3k (поки що).


86
Мені хотілося б побачити детальну розробку щодо "статичного набору тексту" / точки декоратора, без пейоративів. Цитування відсутніх третіх осіб не є корисним. __slots__не стосується тих самих проблем, що й статичне введення тексту. Наприклад, у C ++ не декларування змінної-члена обмежується, це присвоєння невмисному типу (і примусовому компілятору) цій змінній. Я не потураю використанню __slots__, просто зацікавлений у розмові. Дякую!
hiwaylon

126

Ви б хотіли використовувати, __slots__якщо збираєтесь інстанціювати багато (сотні, тисячі) об'єктів одного класу. __slots__існує лише як засіб оптимізації пам'яті.

Вкрай не рекомендується використовувати __slots__для обмеження створення атрибутів.

Перебирання об'єктів, __slots__які не працюватимуть із протоколом соління за замовчуванням (найстарший) необхідно вказати більш пізню версію.

Деякі інші особливості самоаналізу пітона також можуть негативно впливати.


10
Я демонструю підрив прорізаного предмета у своїй відповіді, а також адресую першу частину вашої відповіді.
Зал Аарона

2
Я бачу вашу думку, але слоти пропонують і швидший доступ до атрибутів (як це заявили інші). У цьому випадку вам не потрібно "інстанціювати багато (сотні, тисячі) об'єктів одного класу" , щоб отримати продуктивність. Натомість вам потрібно багато доступу до того ж (прорізного) атрибуту того ж екземпляра. (Будь ласка, виправте мене, якщо я помиляюся.)
Rotareti

61

Кожен об’єкт python має __dict__attribute - словник, що містить усі інші атрибути. наприклад, коли ви вводите self.attrpython насправді робить self.__dict__['attr']. Як ви можете уявити, використання словника для зберігання атрибуту вимагає додаткового місця та часу для доступу до нього.

Однак при використанні __slots__будь-який об’єкт, створений для цього класу, не матиме __dict__атрибута. Натомість весь доступ до атрибутів здійснюється безпосередньо через покажчики.

Отже, якщо ви хочете структуру стилю C, а не повноцінний клас, ви можете використовувати __slots__для ущільнення розмірів об'єктів та скорочення часу доступу до атрибутів. Хороший приклад - клас Point, що містить атрибути x & y. Якщо у вас буде багато очок, ви можете спробувати використовувати __slots__, щоб зберегти деяку пам'ять.


10
Ні, екземпляр класу з __slots__визначеними не схожий на C-стиль структури. Існує іменне атрибутове відображення словника на рівні класу до індексів, інакше таке неможливо: class A(object): __slots__= "value",\n\na=A(); setattr(a, 'value', 1)я дійсно думаю, що цю відповідь слід уточнити (я можу це зробити, якщо хочете). Крім того, я не впевнений, що instance.__hidden_attributes[instance.__class__[attrname]]це швидше instance.__dict__[attrname].
tzot

22

Окрім інших відповідей, ось приклад використання __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 разів за рахунок необхідності ввести спеціальний код підбору, якщо і коли це стане необхідним.


11

Слоти дуже корисні для викликів бібліотеки для усунення "названого методу відправки" під час здійснення функціональних дзвінків. Про це йдеться в документації SWIG . Для бібліотек з високою продуктивністю, які хочуть зменшити накладні витрати для часто званих функцій, використовуючи слоти, це набагато швидше.

Зараз це може не бути безпосередньо пов'язане з питанням про ОП. Це пов’язано більше зі створенням розширень, ніж із використанням синтаксису слотів на об’єкті. Але це допомагає доповнити картину для використання слотів та деяких міркувань за ними.


7

Атрибут екземпляра класу має 3 властивості: екземпляр, ім'я атрибута та значення атрибута.

У звичайному доступі до атрибутів екземпляр діє як словник, а ім'я атрибута виступає в якості ключа в словнику, що шукає значення.

instance (attribute) -> значення

У доступі __slots__ назва атрибута виступає як словник, а екземпляр виступає ключем у словнику, що шукає значення.

атрибут (екземпляр) -> значення

У малюнку легкої ваги назва атрибута виступає як словник, а значення виступає ключовим у словнику, шукаючи екземпляр.

атрибут (значення) -> екземпляр


Це хороша частка, і не вдасться добре в коментарі до одного з відповідей, які також пропонують пролітати, але це не є повною відповіддю на саме запитання. Зокрема (лише в контексті запитання): чому Flyweight та "які випадки слід уникати ..." __slots__?
Мерлін Морган-Грем

@Merlyn Morgan-Graham, це слугує підказкою, на якій можна вибрати: регулярний доступ, __slots__ або легковагова вага.
Дмитро Рубанович

3

Дуже простий приклад __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 ).

Припустимо, якщо у мого класу Тест є тисячі таких об’єктів? Створення додаткового атрибута dictдля кожного об'єкта спричинить великі накладні витрати (пам'ять, обчислювальна потужність тощо) у моєму коді.

Рішення: С __slots__

Тепер у наступному прикладі тестування мого класу міститься __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'

2

Ще одне дещо незрозуміле використання __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

1

У вас - по суті, немає жодної користі __slots__.

За той час , коли ви думаєте , що вам можуть знадобитися __slots__, ви на справді хочете використовувати полегшені або найлегшій шаблони проектування. Це випадки, коли ви більше не хочете використовувати суто об'єкти Python. Замість цього ви хочете обгортати об’єкт Python навколо масиву, структури або numpy масиву.

class Flyweight(object):

    def get(self, theData, index):
        return theData[index]

    def set(self, theData, index, value):
        theData[index]= value

Класоподібна обгортка не має атрибутів - вона просто надає методи, що діють на основні дані. Методи можна звести до методів класу. Дійсно, його можна звести до лише функцій, що працюють на базовому масиві даних.


17
Що має Flyweight до __slots__?
oefe

3
@oefe: Я точно не знаю вашого запитання. Я можу процитувати свою відповідь, якщо це допомагає "коли ви думаєте, що вам можуть знадобитися слоти , ви насправді хочете використовувати ... Легкий шаблон дизайну". Ось що стосується Flyweight з слотами . У вас є конкретніше запитання?
С.Лотт

21
@oefe: Легка вага і __slots__є обома методами оптимізації для збереження пам'яті. __slots__показує переваги, коли у вас багато багатьох об'єктів, а також модель Flyweight Design. Обидва вирішують ту саму проблему.
jfs

7
Чи є порівняння між слотами та Flyweight щодо споживання пам’яті та швидкості?
kontulai

8
Хоча Flyweight, безумовно, корисний у деяких контекстах, вірите чи ні, відповідь на питання "як я можу зменшити використання пам'яті в Python, коли я створюю об'єкти на мільйон", не завжди "не використовуйте Python для ваших об'єктів на мільйон". Іноді __slots__дійсно є відповіддю, і як зазначає Євгені, його можна додати як просту думку (наприклад, спочатку можна сфокусуватись на правильності, а потім додати продуктивність).
Патрік Моупін

0

Первісне питання стосувалося випадків загального користування, а не лише про пам'ять. Тому тут слід зазначити, що ви також отримуєте кращу ефективність при інстанціюванні великої кількості об'єктів - що цікаво, наприклад, при синтаксичному аналізі великих документів на об'єкти або з бази даних.

Ось порівняння створення об’єктних дерев з мільйоном записів, використовуючи слоти та без слотів. В якості довідки також є ефективність використання звичайних диктів для дерев (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

Тестові класи (ident, appart від слотів):

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.