Verwendung von __slots__?

Antworten:

1018

Was ist in Python der Zweck __slots__und in welchen Fällen sollte dies vermieden werden?

TLDR:

Mit dem speziellen Attribut __slots__können Sie explizit angeben, welche Instanzattribute Ihre Objektinstanzen mit den erwarteten Ergebnissen haben sollen:

  1. schnellerer Attributzugriff.
  2. Platzersparnis im Speicher.

Die Platzersparnis ist von

  1. Speichern von Wertreferenzen in Slots anstelle von __dict__.
  2. Verweigern __dict__und __weakref__Erstellen, wenn übergeordnete Klassen sie verweigern und Sie deklarieren __slots__.

Schnelle Vorsichtsmaßnahmen

Kleine Einschränkung: Sie sollten einen bestimmten Slot nur einmal in einem Vererbungsbaum deklarieren. Zum Beispiel:

class Base:
    __slots__ = 'foo', 'bar'

class Right(Base):
    __slots__ = 'baz', 

class Wrong(Base):
    __slots__ = 'foo', 'bar', 'baz'        # redundant foo and bar

Python erhebt keine Einwände, wenn Sie dies falsch verstehen (dies sollte wahrscheinlich der Fall sein). Andernfalls treten möglicherweise keine Probleme auf, aber Ihre Objekte belegen mehr Platz als sie sonst sollten. Python 3.8:

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

Dies liegt daran, dass der Slot-Deskriptor der Basis einen vom Falschen getrennten Slot hat. Dies sollte normalerweise nicht auftauchen, aber es könnte:

>>> 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'

Die größte Einschränkung betrifft die Mehrfachvererbung - mehrere "übergeordnete Klassen mit nicht leeren Slots" können nicht kombiniert werden.

Um dieser Einschränkung Rechnung zu tragen, befolgen Sie die Best Practices: Berücksichtigen Sie die Abstraktion aller bis auf einen oder alle Eltern, von der ihre konkrete Klasse bzw. Ihre neue konkrete Klasse gemeinsam erben wird. Geben Sie den Abstraktionen leere Slots (genau wie abstrakte Basisklassen in der.) Standardbibliothek).

Ein Beispiel finden Sie im Abschnitt zur Mehrfachvererbung.

Bedarf:

  • Damit Attribute benannt werden können, __slots__um tatsächlich in Slots anstelle von a gespeichert zu werden __dict__, muss eine Klasse von erben object.

  • Um die Erstellung von a zu verhindern __dict__, müssen Sie von erben objectund alle Klassen in der Vererbung müssen deklarieren, __slots__und keine von ihnen kann einen '__dict__'Eintrag haben.

Es gibt viele Details, wenn Sie weiterlesen möchten.

Warum verwenden __slots__: Schnellerer Zugriff auf Attribute.

Der Schöpfer von Python, Guido van Rossum, gibt an, dass er tatsächlich __slots__für einen schnelleren Attributzugriff erstellt hat.

Es ist trivial, einen messbar signifikanten schnelleren Zugriff nachzuweisen:

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

und

>>> min(timeit.repeat(get_set_delete_fn(slotted)))
0.2846834529991611
>>> min(timeit.repeat(get_set_delete_fn(not_slotted)))
0.3664822799983085

Der Slot-Zugriff ist in Python 3.5 unter Ubuntu fast 30% schneller.

>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342

In Python 2 unter Windows habe ich es ungefähr 15% schneller gemessen.

Warum verwenden __slots__: Speichereinsparungen

Ein weiterer Zweck von __slots__besteht darin, den Speicherplatz zu reduzieren, den jede Objektinstanz belegt.

In meinem eigenen Beitrag zur Dokumentation sind die Gründe dafür klar angegeben :

Der durch die Verwendung eingesparte Speicherplatz __dict__kann erheblich sein.

SQLAlchemy schreibt eine Menge Speichereinsparungen zu __slots__.

Um dies zu überprüfen, verwendet die Anaconda-Distribution von Python 2.7 unter Ubuntu Linux mit guppy.hpy(auch bekannt als heapy) und sys.getsizeofdie Größe einer Klasseninstanz ohne __slots__Deklaration und sonst nichts 64 Byte. Das macht nicht enthalten die __dict__. Vielen Dank an Python für die verzögerte Auswertung. Das __dict__wird anscheinend erst ins Leben gerufen, wenn darauf verwiesen wird, aber Klassen ohne Daten sind normalerweise nutzlos. Beim __dict__Aufrufen beträgt das Attribut zusätzlich mindestens 280 Byte.

Im Gegensatz dazu besteht eine Klasseninstanz mit __slots__deklariert als ()(keine Daten) nur aus 16 Bytes und 56 Gesamtbytes mit einem Element in Slots, 64 mit zwei.

Für 64-Bit-Python zeige ich den Speicherverbrauch in Bytes in Python 2.7 und 3.6 für __slots__und __dict__(keine Slots definiert) für jeden Punkt, an dem das Diktat in 3.6 wächst (mit Ausnahme der Attribute 0, 1 und 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

Trotz kleinerer Dikte in Python 3 sehen wir, wie gut sich __slots__Instanzen skalieren lassen, um Speicherplatz zu sparen, und das ist ein Hauptgrund, den Sie verwenden möchten __slots__.

Beachten Sie zur Vollständigkeit meiner Notizen, dass im Namespace der Klasse einmalige Kosten pro Slot von 64 Byte in Python 2 und 72 Byte in Python 3 anfallen, da Slots Datendeskriptoren wie Eigenschaften verwenden, die als "Mitglieder" bezeichnet werden.

>>> Foo.foo
<member 'foo' of 'Foo' objects>
>>> type(Foo.foo)
<class 'member_descriptor'>
>>> getsizeof(Foo.foo)
72

Demonstration von __slots__:

Um die Erstellung von a zu verweigern __dict__, müssen Sie folgende Unterklassen erstellen object:

class Base(object): 
    __slots__ = ()

jetzt:

>>> 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'

Oder eine andere Klasse unterordnen, die definiert __slots__

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

und nun:

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

aber:

>>> 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'

Damit __dict__Schaffung während geschlitzten Objekte Subklassen, fügen Sie einfach '__dict__'auf die __slots__(beachten Sie, dass Schlitze bestellt werden, und Sie sollten nicht wiederholen Slots , die bereits in übergeordneten Klassen sind):

class SlottedWithDict(Child): 
    __slots__ = ('__dict__', 'b')

swd = SlottedWithDict()
swd.a = 'a'
swd.b = 'b'
swd.c = 'c'

und

>>> swd.__dict__
{'c': 'c'}

Oder Sie müssen nicht einmal __slots__in Ihrer Unterklasse deklarieren , und Sie verwenden weiterhin Slots der Eltern, ohne die Erstellung eines __dict__:

class NoSlots(Child): pass
ns = NoSlots()
ns.a = 'a'
ns.b = 'b'

Und:

>>> ns.__dict__
{'b': 'b'}

Allerdings __slots__kann zu Problemen führen , für Mehrfachvererbung:

class BaseA(object): 
    __slots__ = ('a',)

class BaseB(object): 
    __slots__ = ('b',)

Da das Erstellen einer untergeordneten Klasse von Eltern mit beiden nicht leeren Steckplätzen fehlschlägt:

>>> 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

Wenn Sie auf dieses Problem stoßen, können Sie es einfach __slots__von den Eltern entfernen oder, wenn Sie die Kontrolle über die Eltern haben, ihnen leere Slots geben oder Abstraktionen umgestalten:

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!

Hinzufügen '__dict__', __slots__um eine dynamische Zuordnung zu erhalten:

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

und nun:

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

Mit '__dict__'In-Slots verlieren wir einige der Größenvorteile, da wir eine dynamische Zuweisung haben und immer noch Slots für die Namen haben, die wir erwarten.

Wenn Sie von einem Objekt erben, das nicht mit einem Slot versehen ist, erhalten Sie bei der Verwendung dieselbe Semantik __slots__- Namen, die __slots__auf Slot-Werte verweisen, während andere Werte in die Instanz eingefügt werden __dict__.

Vermeiden, __slots__weil Sie in der Lage sein möchten, Attribute im laufenden Betrieb hinzuzufügen, ist eigentlich kein guter Grund - fügen "__dict__"Sie einfach Ihre hinzu , __slots__wenn dies erforderlich ist.

Sie können auch explizit hinzufügen __weakref__, __slots__wenn Sie diese Funktion benötigen.

Auf leeres Tupel setzen, wenn ein benanntes Tupel untergeordnet wird:

Das eingebaute Namedtuple macht unveränderliche Instanzen, die sehr leicht sind (im Wesentlichen die Größe von Tupeln). Um jedoch die Vorteile zu nutzen, müssen Sie dies selbst tun, wenn Sie sie in Unterklassen unterteilen:

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

Verwendungszweck:

>>> nt = MyNT('bar', 'baz')
>>> nt.bar
'bar'
>>> nt.baz
'baz'

Der Versuch, ein unerwartetes Attribut zuzuweisen, führt zu einem, AttributeErrorweil wir die Erstellung von Folgendem verhindert haben __dict__:

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

Sie können die __dict__Erstellung zulassen, indem Sie sie weglassen __slots__ = (), aber Sie können nicht nicht leer __slots__mit Subtypen von Tupeln verwenden.

Größte Einschränkung: Mehrfachvererbung

Selbst wenn nicht leere Steckplätze für mehrere Eltern gleich sind, können sie nicht zusammen verwendet werden:

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

Mit einem leeren __slots__in den Eltern scheint die Flexibilität zu bieten, das Kind ermöglicht zu wählen , zu verhindern oder zu ermöglichen (durch Hinzufügen '__dict__'dynamische Zuordnung zu erhalten, siehe Abschnitt oben) die Schaffung eines__dict__ :

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

Sie müssen nicht haben zu Slots - also , wenn Sie sie hinzufügen, und entfernen Sie sie später, es keine Probleme verursachen sollte.

Hier auf die Nerven gehen : Wenn Sie Mixins komponieren oder abstrakte Basisklassen verwenden , die nicht instanziiert werden sollen, __slots__scheint ein Leerzeichen in diesen Eltern der beste Weg zu sein, um die Flexibilität für Unterklassen zu verbessern.

Um dies zu demonstrieren, erstellen wir zunächst eine Klasse mit Code, den wir unter Mehrfachvererbung verwenden möchten

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)})'

Wir könnten das Obige direkt verwenden, indem wir die erwarteten Slots erben und deklarieren:

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

Aber das interessiert uns nicht, das ist eine triviale Einzelvererbung. Wir brauchen eine andere Klasse, von der wir möglicherweise auch erben, vielleicht mit einem lauten Attribut:

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

Wenn beide Basen nicht leere Slots hätten, könnten wir das Folgende nicht tun. (Wenn wir wollten, hätten wir tatsächlich AbstractBasenicht leere Slots a und b vergeben und sie aus der folgenden Erklärung herauslassen können - es wäre falsch, sie zu belassen):

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

Und jetzt haben wir Funktionalität von beiden über Mehrfachvererbung und können immer noch verweigern __dict__und __weakref__instanziieren:

>>> 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'

Andere Fälle, um Slots zu vermeiden:

  • Vermeiden Sie sie, wenn Sie eine __class__Zuweisung mit einer anderen Klasse durchführen möchten, die sie nicht hat (und Sie können sie nicht hinzufügen), es sei denn, die Steckplatzlayouts sind identisch. (Ich bin sehr daran interessiert zu erfahren, wer dies tut und warum.)
  • Vermeiden Sie sie, wenn Sie Buildins mit variabler Länge wie long, tuple oder str in Unterklassen unterteilen und ihnen Attribute hinzufügen möchten.
  • Vermeiden Sie sie, wenn Sie darauf bestehen, Standardwerte über Klassenattribute für Instanzvariablen bereitzustellen.

Möglicherweise können Sie weitere Vorbehalte aus dem Rest der __slots__ Dokumentation herausarbeiten (die 3.7-Entwicklerdokumente sind die aktuellsten) , zu denen ich in letzter Zeit wichtige Beiträge geleistet habe.

Kritik anderer Antworten

Die aktuellen Top-Antworten zitieren veraltete Informationen und sind ziemlich handgewellt und verfehlen die Marke in einigen wichtigen Punkten.

Nicht "nur zum __slots__Instanziieren vieler Objekte verwenden"

Ich zitiere:

"Sie möchten verwenden, __slots__wenn Sie viele (Hunderte, Tausende) Objekte derselben Klasse instanziieren möchten ."

Abstrakte Basisklassen, beispielsweise aus dem collectionsModul, werden nicht instanziiert, sondern __slots__für sie deklariert.

Warum?

Wenn ein Benutzer die Erstellung verweigern __dict__oder __weakref__erstellen möchte , dürfen diese Dinge in den übergeordneten Klassen nicht verfügbar sein.

__slots__ trägt zur Wiederverwendbarkeit beim Erstellen von Schnittstellen oder Mixins bei.

Es ist wahr, dass viele Python-Benutzer nicht für die Wiederverwendbarkeit schreiben, aber wenn Sie dies tun, ist es wertvoll, die Option zu haben, unnötige Speicherplatznutzung zu verweigern.

__slots__ bricht das Beizen nicht

Wenn Sie ein geschlitztes Objekt beizen, kann es sein, dass es irreführend ist TypeError:

>>> pickle.loads(pickle.dumps(f))
TypeError: a class that defines __slots__ without defining __getstate__ cannot be pickled

Das ist eigentlich falsch. Diese Nachricht stammt aus dem ältesten Protokoll, das die Standardeinstellung ist. Sie können das neueste Protokoll mit dem -1Argument auswählen . In Python 2.7 wäre dies 2(was in 2.3 eingeführt wurde) und in 3.6 ist es 4.

>>> pickle.loads(pickle.dumps(f, -1))
<__main__.Foo object at 0x1129C770>

in Python 2.7:

>>> pickle.loads(pickle.dumps(f, 2))
<__main__.Foo object at 0x1129C770>

in Python 3.6

>>> pickle.loads(pickle.dumps(f, 4))
<__main__.Foo object at 0x1129C770>

Daher würde ich dies berücksichtigen, da es sich um ein gelöstes Problem handelt.

Kritik an der (bis 2. Oktober 2016) akzeptierten Antwort

Der erste Absatz ist eine halb kurze Erklärung, halb eine Vorhersage. Hier ist der einzige Teil, der die Frage tatsächlich beantwortet

Die richtige Verwendung __slots__ist, um Platz in Objekten zu sparen. Anstatt ein dynamisches Diktat zu haben, mit dem Objekte jederzeit mit Attributen versehen werden können, gibt es eine statische Struktur, die nach der Erstellung keine Ergänzungen zulässt. Dies spart den Overhead eines Diktats für jedes Objekt, das Slots verwendet

Die zweite Hälfte ist Wunschdenken und falsch:

Obwohl dies manchmal eine nützliche Optimierung ist, wäre es völlig unnötig, wenn der Python-Interpreter dynamisch genug wäre, so dass er nur dann das Diktat benötigt, wenn das Objekt tatsächlich ergänzt wurde.

Python macht tatsächlich etwas Ähnliches und erstellt nur das, __dict__wenn darauf zugegriffen wird, aber das Erstellen vieler Objekte ohne Daten ist ziemlich lächerlich.

Der zweite Absatz vereinfacht und übersieht tatsächliche Gründe, die vermieden werden sollten __slots__. Das Folgende ist kein wirklicher Grund, Slots zu vermeiden (aus tatsächlichen Gründen siehe den Rest meiner Antwort oben):

Sie ändern das Verhalten von Objekten mit Slots auf eine Weise, die von Kontrollfreaks und statischen Tippern missbraucht werden kann.

Anschließend werden andere Möglichkeiten zur Erreichung dieses perversen Ziels mit Python erörtert, ohne dass etwas damit zu tun hat __slots__.

Der dritte Absatz ist Wunschdenken. Zusammen handelt es sich meistens um unkonventionelle Inhalte, die der Antwortende nicht einmal verfasst hat und die zur Munition für Kritiker der Website beitragen.

Hinweise zur Speichernutzung

Erstellen Sie einige normale Objekte und geschlitzte Objekte:

>>> class Foo(object): pass
>>> class Bar(object): __slots__ = ()

Instanziieren Sie eine Million von ihnen:

>>> foos = [Foo() for f in xrange(1000000)]
>>> bars = [Bar() for b in xrange(1000000)]

Inspizieren mit 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
...

Greifen Sie auf die regulären Objekte und deren Objekte zu __dict__und überprüfen Sie sie erneut:

>>> 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
...

Dies steht im Einklang mit der Geschichte von Python aus der Vereinheitlichung von Typen und Klassen in Python 2.2

Wenn Sie einen integrierten Typ unterordnen, wird den Instanzen automatisch zusätzlicher Speicherplatz hinzugefügt, um __dict__und zu berücksichtigen __weakrefs__. (Das __dict__wird jedoch erst initialisiert, wenn Sie es verwenden. Sie sollten sich also keine Gedanken über den Speicherplatz machen, den ein leeres Wörterbuch für jede von Ihnen erstellte Instanz belegt.) Wenn Sie diesen zusätzlichen Speicherplatz nicht benötigen, können Sie den Ausdruck " __slots__ = []" hinzufügen deine Klasse.

Aaron Hall
quelle
14
Wow, eine verdammt gute Antwort - danke! Allerdings habe ich das class Child(BaseA, BaseB): __slots__ = ('a', 'b')Beispiel mit den Empy-Slot-Eltern nicht verstanden. Warum wird hier ein für dictproxyerstellt, anstatt ein AttributeErrorfür zu erhöhen c?
Skandix
@ Skandix, danke, dass du mich auf diesen Tippfehler aufmerksam gemacht hast. Es stellte sich heraus, dass c keine Instanziierung war. Ich habe wahrscheinlich vergessen, dass ich diesen Teil bearbeitet habe, als ich ihn im Post-Verlauf gespeichert habe. Es wäre wahrscheinlich früher gefangen worden, wenn ich das Richtige getan und den Code kopierfähiger gemacht hätte ... Nochmals vielen Dank!
Aaron Hall
38
Diese Antwort sollte Teil der offiziellen Python-Dokumentation zu sein __slots__. Ernsthaft! Vielen Dank!
NightElfik
13
@ NightElfik Ob Sie es glauben oder nicht, ich habe vor __slots__ungefähr einem Jahr zu den Python-Dokumenten beigetragen : github.com/python/cpython/pull/1819/files
Aaron Hall
Fantastisch detaillierte Antwort. Ich habe eine Frage: Sollte man Slots als Standard verwenden, es sei denn, die Verwendung trifft eine der Einschränkungen, oder sollten Slots berücksichtigt werden, wenn Sie wissen, dass Sie um Geschwindigkeit / Speicher kämpfen werden? Um es anders auszudrücken, sollten Sie einen Neuling ermutigen, etwas über sie zu lernen und sie von Anfang an zu verwenden?
Freethebees
265

Zitat Jacob Hallen :

Die richtige Verwendung __slots__ist, um Platz in Objekten zu sparen. Anstatt ein dynamisches Diktat zu haben, mit dem Objekte jederzeit mit Attributen versehen werden können, gibt es eine statische Struktur, die nach der Erstellung keine Ergänzungen zulässt. [Diese Verwendung von __slots__eliminiert den Overhead eines Diktats für jedes Objekt.] Obwohl dies manchmal eine nützliche Optimierung ist, wäre es völlig unnötig, wenn der Python-Interpreter dynamisch genug wäre, so dass er das Diktat nur dann benötigt, wenn es tatsächlich Ergänzungen zum Objekt gibt Objekt.

Leider haben Slots einen Nebeneffekt. Sie ändern das Verhalten von Objekten mit Slots auf eine Weise, die von Kontrollfreaks und statischen Tippern missbraucht werden kann. Das ist schlecht, weil die Kontrollfreaks die Metaklassen missbrauchen sollten und die statischen Tipp-Weenies Dekorateure missbrauchen sollten, da es in Python nur einen offensichtlichen Weg geben sollte, etwas zu tun.

CPython intelligent genug zu machen, um Platz zu sparen, ohne __slots__es zu sparen, ist ein großes Unterfangen, weshalb es wahrscheinlich (noch) nicht auf der Liste der Änderungen für P3k steht.

Jeff Bauer
quelle
86
Ich würde gerne etwas näher auf den Punkt "statische Typisierung" / Dekorateur eingehen, ohne Abwertungen. Das Zitieren abwesender Dritter ist nicht hilfreich. __slots__behandelt nicht die gleichen Probleme wie die statische Eingabe. In C ++ wird beispielsweise nicht die Deklaration einer Mitgliedsvariablen eingeschränkt, sondern die Zuweisung eines unbeabsichtigten Typs (und des erzwungenen Compilers) zu dieser Variablen. Ich dulde nicht die Verwendung von __slots__, nur an dem Gespräch interessiert. Vielen Dank!
Hiwaylon
126

Sie möchten verwenden, __slots__wenn Sie viele (Hunderte, Tausende) Objekte derselben Klasse instanziieren möchten . __slots__existiert nur als Speicheroptimierungswerkzeug.

Es wird dringend davon abgeraten, die __slots__Attributerstellung einzuschränken.

Das Beizen von Objekten mit __slots__funktioniert nicht mit dem Standard (ältesten) Beizprotokoll. Es ist erforderlich, eine spätere Version anzugeben.

Einige andere Introspektionsmerkmale von Python können ebenfalls nachteilig beeinflusst werden.

Ryan
quelle
10
Ich demonstriere das Beizen eines geschlitzten Objekts in meiner Antwort und spreche auch den ersten Teil Ihrer Antwort an.
Aaron Hall
2
Ich verstehe Ihren Standpunkt, aber Slots bieten auch einen schnelleren Zugriff auf Attribute (wie andere angegeben haben). In diesem Fall müssen Sie nicht "viele (Hunderte, Tausende) Objekte derselben Klasse instanziieren" , um Leistung zu erzielen. Stattdessen benötigen Sie viele Zugriffe auf dasselbe (geschlitzte) Attribut derselben Instanz. (Bitte korrigieren Sie mich, wenn ich falsch
liege
61

Jedes Python-Objekt verfügt über ein __dict__Attribut, bei dem es sich um ein Wörterbuch handelt, das alle anderen Attribute enthält. Beispiel: Wenn Sie self.attrPython eingeben, ist dies tatsächlich der Fall self.__dict__['attr']. Wie Sie sich vorstellen können, benötigt die Verwendung eines Wörterbuchs zum Speichern von Attributen zusätzlichen Speicherplatz und Zeit für den Zugriff darauf.

Wenn Sie jedoch verwenden __slots__, hat jedes für diese Klasse erstellte Objekt kein __dict__Attribut. Stattdessen erfolgt der gesamte Attributzugriff direkt über Zeiger.

Wenn Sie also eine Struktur im C-Stil anstelle einer vollwertigen Klasse wünschen, können Sie die __slots__Größe der Objekte komprimieren und die Zugriffszeit für Attribute reduzieren. Ein gutes Beispiel ist eine Point-Klasse mit den Attributen x & y. Wenn Sie viele Punkte haben, können Sie versuchen, __slots__etwas Speicherplatz zu sparen.

Suraj
quelle
10
Nein, eine Instanz einer Klasse mit __slots__definiert ist nicht wie eine C-Struktur. Es gibt ein Wörterbuch auf Klassenebene, das Attributnamen Indizes zuordnet, andernfalls wäre Folgendes nicht möglich: class A(object): __slots__= "value",\n\na=A(); setattr(a, 'value', 1)Ich denke wirklich, dass diese Antwort geklärt werden sollte (ich kann das tun, wenn Sie möchten). Ich bin mir auch nicht sicher, ob instance.__hidden_attributes[instance.__class__[attrname]]das schneller ist als instance.__dict__[attrname].
Zot
22

Zusätzlich zu den anderen Antworten finden Sie hier ein Beispiel für die Verwendung von __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']

Für die Implementierung __slots__ist nur eine zusätzliche Zeile erforderlich (und Ihre Klasse wird zu einer Klasse neuen Stils, sofern dies noch nicht geschehen ist). Auf diese Weise können Sie den Speicherbedarf dieser Klassen um das Fünffache reduzieren , auf Kosten der Notwendigkeit, benutzerdefinierten Pickle-Code zu schreiben, falls dies erforderlich wird.

Evgeni Sergeev
quelle
11

Slots sind sehr nützlich für Bibliotheksaufrufe, um den "Named Method Dispatch" bei Funktionsaufrufen zu eliminieren. Dies wird in der SWIG- Dokumentation erwähnt . Für Hochleistungsbibliotheken, die den Funktionsaufwand für häufig aufgerufene Funktionen mithilfe von Slots reduzieren möchten, ist dies viel schneller.

Dies hängt möglicherweise nicht direkt mit der OP-Frage zusammen. Es bezieht sich mehr auf das Erstellen von Erweiterungen als auf die Verwendung der Slots- Syntax für ein Objekt. Aber es hilft, das Bild für die Verwendung von Slots und einige der Gründe dafür zu vervollständigen.

Demolishun
quelle
7

Ein Attribut einer Klasseninstanz hat drei Eigenschaften: die Instanz, den Namen des Attributs und den Wert des Attributs.

Beim regulären Attributzugriff fungiert die Instanz als Wörterbuch, und der Name des Attributs fungiert als Schlüssel in diesem Wörterbuch, der nach dem Wert sucht.

Instanz (Attribut) -> Wert

Beim Zugriff auf __slots__ fungiert der Name des Attributs als Wörterbuch und die Instanz als Schlüssel im Wörterbuch, der nach dem Wert sucht.

Attribut (Instanz) -> Wert

Im Flyweight-Muster fungiert der Name des Attributs als Wörterbuch und der Wert als Schlüssel in diesem Wörterbuch, das die Instanz nachschlägt.

Attribut (Wert) -> Instanz

Dmitry Rubanovich
quelle
Dies ist eine gute Aktie und passt nicht gut in einen Kommentar zu einer der Antworten, die auch Fliehgewichte vorschlagen, aber es ist keine vollständige Antwort auf die Frage selbst. Insbesondere (nur im Zusammenhang mit der Frage): Warum Fliegengewicht und "Was sind die Fälle, die man vermeiden sollte ..." __slots__?
Merlyn Morgan-Graham
@Merlyn Morgan-Graham, dient als Hinweis für die Auswahl: regulärer Zugriff, __slots__ oder Fliegengewicht.
Dmitry Rubanovich
3

Ein sehr einfaches Beispiel für ein __slot__Attribut.

Problem: Ohne __slots__

Wenn ich kein __slot__Attribut in meiner Klasse habe, kann ich meinen Objekten neue Attribute hinzufügen.

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}

Wenn Sie sich das obige Beispiel ansehen, können Sie sehen, dass obj1 und obj2 ihre eigenen x- und y- Attribute haben und Python auch ein dictAttribut für jedes Objekt erstellt hat ( obj1 und obj2 ).

Nehmen wir an, wenn meine Klasse - Test Tausende solcher Objekte hat? Das Erstellen eines zusätzlichen Attributs dictfür jedes Objekt verursacht viel Overhead (Speicher, Rechenleistung usw.) in meinem Code.

Lösung: Mit __slots__

Im folgenden Beispiel enthält meine Klasse Test ein__slots__ Attribut. Jetzt kann ich meinen Objekten keine neuen Attribute hinzufügen (außer Attributen x) und Python erstellt kein dictAttribut mehr. Dadurch entfällt der Overhead für jedes Objekt, der bei vielen Objekten erheblich werden kann.

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'
N Randhawa
quelle
2

Eine andere etwas undurchsichtige Verwendung __slots__ist das Hinzufügen von Attributen zu einem Objekt-Proxy aus dem ProxyTypes-Paket, das früher Teil des PEAK-Projekts war. Sein ObjectWrapperermöglicht es Ihnen , ein anderes Objekt zu Proxy, aber abfangen alle Interaktionen mit dem Proxy - Objekt. Es wird nicht sehr häufig verwendet (und bietet keine Python 3-Unterstützung), aber wir haben es verwendet, um einen thread-sicheren Blockierungs-Wrapper um eine auf Tornado basierende asynchrone Implementierung zu implementieren, der den gesamten Zugriff auf das Proxy-Objekt über ioloop mithilfe von thread-safe abprallt concurrent.FutureObjekte zum Synchronisieren und Zurückgeben von Ergebnissen.

Standardmäßig erhalten Sie bei jedem Attributzugriff auf das Proxy-Objekt das Ergebnis des Proxy-Objekts. Wenn Sie dem Proxy-Objekt ein Attribut hinzufügen müssen, __slots__kann es verwendet werden.

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
NeilenMarais
quelle
1

Sie haben - im Wesentlichen - keine Verwendung für __slots__.

Für die Zeit, in der Sie glauben, dass Sie sie benötigen __slots__, möchten Sie tatsächlich Lightweight- oder Flyweight- Designmuster verwenden. Dies sind Fälle, in denen Sie keine reinen Python-Objekte mehr verwenden möchten. Stattdessen möchten Sie einen Python-Objekt-ähnlichen Wrapper um ein Array, eine Struktur oder ein Numpy-Array.

class Flyweight(object):

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

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

Der klassenähnliche Wrapper hat keine Attribute - er bietet nur Methoden, die auf die zugrunde liegenden Daten wirken. Die Methoden können auf Klassenmethoden reduziert werden. In der Tat könnte es auf nur Funktionen reduziert werden, die mit dem zugrunde liegenden Datenarray arbeiten.

S.Lott
quelle
17
Was hat Fliegengewicht damit zu tun __slots__?
Oefe
3
@oefe: Ich verstehe deine Frage sicher nicht. Ich kann meine Antwort zitieren, wenn es hilft, "wenn Sie denken, dass Sie Slots benötigen , möchten Sie tatsächlich ... Flyweight Design Pattern verwenden". Das hat Flyweight mit Slots zu tun . Haben Sie eine spezifischere Frage?
S.Lott
21
@oefe: Fliegengewicht und __slots__sind beide Optimierungstechniken, um Speicherplatz zu sparen. __slots__zeigt Vorteile, wenn Sie viele, viele Objekte sowie Flyweight Design-Muster haben. Beide lösen das gleiche Problem.
JFS
7
Gibt es einen verfügbaren Vergleich zwischen der Verwendung von Steckplätzen und der Verwendung von Flyweight hinsichtlich Speicherverbrauch und Geschwindigkeit?
kontulai
8
Obwohl Flyweight in einigen Kontexten sicherlich nützlich ist, ob Sie es glauben oder nicht, lautet die Antwort auf "Wie kann ich die Speichernutzung in Python reduzieren, wenn ich zig Objekte erstelle?" Nicht immer "Verwenden Sie Python nicht für zig Millionen Objekte". Manchmal __slots__ist das wirklich die Antwort, und wie Evgeni betont, kann sie als einfacher nachträglicher Gedanke hinzugefügt werden (z. B. können Sie sich zuerst auf die Korrektheit konzentrieren und dann die Leistung steigern).
Patrick Maupin
0

Die ursprüngliche Frage betraf allgemeine Anwendungsfälle, nicht nur das Gedächtnis. Daher sollte hier erwähnt werden, dass Sie auch beim Instanziieren großer Objektmengen eine bessere Leistung erzielen - interessant, z. B. beim Parsen großer Dokumente in Objekte oder aus einer Datenbank.

Hier ist ein Vergleich des Erstellens von Objektbäumen mit einer Million Einträgen unter Verwendung von Slots und ohne Slots. Als Referenz auch die Leistung bei der Verwendung von einfachen Diktaten für die Bäume (Py2.7.10 unter 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

Testklassen (ident, getrennt von Slots):

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)

Testcode, ausführlicher Modus:

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
Rote Pille
quelle