Utilizzo di __slots__?


Risposte:


1019

In Python, qual è lo scopo __slots__e quali casi si dovrebbero evitare?

TLDR:

L'attributo speciale __slots__ti consente di indicare esplicitamente quali attributi dell'istanza ti aspetti siano le istanze dell'oggetto, con i risultati previsti:

  1. accesso più veloce agli attributi.
  2. risparmio di spazio in memoria.

Il risparmio di spazio è di

  1. Memorizzare i riferimenti di valore negli slot anziché __dict__.
  2. Negazione __dict__e __weakref__creazione se le classi padre le negano e tu le dichiari __slots__.

Avvertenze rapide

Piccolo avvertimento, dovresti dichiarare una determinata slot una sola volta in un albero ereditario. Per esempio:

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

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

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

Python non si oppone quando sbagli (probabilmente dovrebbe), altrimenti i problemi potrebbero non manifestarsi, ma i tuoi oggetti occuperanno più spazio di quanto dovrebbero altrimenti. Python 3.8:

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

Questo perché il descrittore di slot della Base ha uno slot separato da quello sbagliato. Questo di solito non dovrebbe emergere, ma potrebbe:

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

L'avvertenza più grande riguarda l'ereditarietà multipla: non è possibile combinare più "classi parent con slot non vuoti".

Per soddisfare questa restrizione, segui le migliori pratiche: Fattorizza tutte le astrazioni tranne una o tutte i genitori che la loro classe concreta rispettivamente e la tua nuova classe concreta erediteranno collettivamente - dando alle astrazioni slot vuoti (proprio come le classi base astratte nella libreria standard).

Vedi la sezione sull'ereditarietà multipla di seguito per un esempio.

Requisiti:

  • Affinché gli attributi nominati __slots__siano effettivamente archiviati negli slot anziché in __dict__, una classe deve ereditare object.

  • Per impedire la creazione di a __dict__, è necessario ereditare objecte tutte le classi nell'ereditarietà devono dichiarare __slots__e nessuna di esse può avere una '__dict__'voce.

Ci sono molti dettagli se desideri continuare a leggere.

Perché utilizzare __slots__: accesso più rapido agli attributi.

Il creatore di Python, Guido van Rossum, afferma di aver effettivamente creato __slots__per un accesso più rapido agli attributi.

È banale dimostrare un accesso più rapido misurabile in modo significativo:

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

e

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

L'accesso con slot è quasi il 30% più veloce in Python 3.5 su Ubuntu.

>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342

In Python 2 su Windows l'ho misurato circa il 15% più velocemente.

Perché usare __slots__ : Risparmio di memoria

Un altro scopo di __slots__ è ridurre lo spazio in memoria occupato da ciascuna istanza di oggetto.

Il mio contributo alla documentazione indica chiaramente i motivi alla base di questo :

Lo spazio risparmiato sull'utilizzo __dict__può essere significativo.

SQLAlchemy attribuisce molti risparmi di memoria a __slots__.

Per verificarlo, usando la distribuzione Anaconda di Python 2.7 su Ubuntu Linux, con guppy.hpy(aka heapy) e sys.getsizeof, la dimensione di un'istanza di classe senza __slots__dichiarata, e nient'altro, è di 64 byte. Ciò non include il __dict__. Grazie Python per la valutazione pigra di nuovo, __dict__apparentemente non viene chiamato in esistenza fino a quando non viene referenziato, ma le classi senza dati sono di solito inutili. Quando chiamato in esistenza, l' __dict__attributo è un minimo di 280 byte in più.

Al contrario, un'istanza di classe con __slots__dichiarata essere() (nessun dato) è solo 16 byte e 56 byte totali con un elemento negli slot, 64 con due.

Per Python a 64 bit, illustrerò il consumo di memoria in byte in Python 2.7 e 3.6, per __slots__e __dict__(nessuno slot definito) per ogni punto in cui il dict cresce in 3.6 (tranne per gli attributi 0, 1 e 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

Quindi, nonostante i piccoli dadi in Python 3, vediamo come __slots__ridimensionare piacevolmente le istanze per farci risparmiare memoria, e questo è uno dei motivi principali che vorresti usare __slots__.

Solo per completezza delle mie note, nota che c'è un costo una tantum per slot nello spazio dei nomi della classe di 64 byte in Python 2 e 72 byte in Python 3, perché gli slot usano descrittori di dati come proprietà, chiamati "membri".

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

Dimostrazione di __slots__:

Per negare la creazione di a __dict__, è necessario eseguire la sottoclasse object:

class Base(object): 
    __slots__ = ()

adesso:

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

O sottoclasse un'altra classe che definisce __slots__

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

e adesso:

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

ma:

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

Per consentire la __dict__creazione durante la sottoclasse di oggetti con slot , basta aggiungere '__dict__'a __slots__(nota che gli slot sono ordinati e non dovresti ripetere gli slot che sono già nelle classi principali):

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

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

e

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

Oppure non hai nemmeno bisogno di dichiarare __slots__nella tua sottoclasse e utilizzerai comunque le slot dei genitori, ma non limiterai la creazione di un __dict__:

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

E:

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

Tuttavia, __slots__possono causare problemi per l'ereditarietà multipla:

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

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

Perché la creazione di una classe figlio dai genitori con entrambi gli slot non vuoti non riesce:

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

Se ti imbatti in questo problema, potresti semplicemente rimuoverlo __slots__dai genitori, o se hai il controllo dei genitori, dare loro spazi vuoti o rifattorizzare le astrazioni:

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!

Aggiungi '__dict__'a __slots__per ottenere l'assegnazione dinamica:

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

e adesso:

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

Quindi, con '__dict__'negli slot perdiamo alcuni dei vantaggi delle dimensioni con il vantaggio di avere un'assegnazione dinamica e avere ancora slot per i nomi che ci aspettiamo.

Quando si eredita da un oggetto che non è inserito, si ottiene lo stesso tipo di semantica quando si usano __slots__- nomi che fanno __slots__riferimento a valori di inserimento, mentre qualsiasi altro valore viene inserito nell'istanza __dict__.

Evitare __slots__perché vuoi essere in grado di aggiungere attributi al volo non è in realtà un buon motivo - basta aggiungere "__dict__"al tuo__slots__ se necessario.

È possibile aggiungere in modo simile __weakref__al __slots__modo esplicito se avete bisogno di tale funzionalità.

Imposta per svuotare la tupla durante la sottoclasse di una tupla con nome:

L'integrato namedtuple crea istanze immutabili che sono molto leggere (essenzialmente, la dimensione delle tuple) ma per ottenere i benefici, è necessario farlo da soli se li si esegue la sottoclasse:

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

utilizzo:

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

E il tentativo di assegnare un attributo imprevisto genera un AttributeErrorperché abbiamo impedito la creazione di __dict__:

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

È possibile consentire __dict__la creazione, lasciando fuori__slots__ = () , ma non è possibile utilizzare non vuota __slots__con sottotipi di tuple.

Il più grande avvertimento: eredità multipla

Anche quando gli slot non vuoti sono gli stessi per più genitori, non possono essere usati insieme:

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

L'uso di un vuoto __slots__nel genitore sembra fornire la massima flessibilità, consentendo al bambino di scegliere di impedire o consentire (aggiungendo '__dict__'per ottenere un compito dinamico, vedere la sezione sopra) la creazione di un__dict__ :

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

Non si hanno di avere slot - quindi se li si aggiunge, e rimuoverli in seguito, non dovrebbe causare problemi.

Andare su un arto qui : se stai componendo mixin o usi classi base astratte , che non sono pensate per essere istanziate, un vuoto __slots__in quei genitori sembra essere il modo migliore per andare in termini di flessibilità per le sottoclassi.

Per dimostrare, per prima cosa, creiamo una classe con codice che vorremmo usare in eredità multipla

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

Potremmo utilizzare quanto sopra direttamente ereditando e dichiarando gli slot previsti:

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

Ma non ci interessa, è una banale eredità singola, abbiamo bisogno di un'altra classe da cui possiamo anche ereditare, forse con un attributo rumoroso:

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

Ora se entrambe le basi avessero slot vuoti, non potremmo fare quanto segue. (In effetti, se volessimo, avremmo potuto dare AbstractBaseslot vuoti aeb, lasciandoli fuori dalla seguente dichiarazione - lasciandoli dentro sarebbe sbagliato):

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

E ora abbiamo funzionalità da entrambi tramite ereditarietà multipla e possiamo ancora negare __dict__e creare __weakref__istanze:

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

Altri casi per evitare slot:

  • Evitali quando vuoi esibirti __class__ compito con un'altra classe che non li ha (e non puoi aggiungerli) a meno che i layout degli slot siano identici. (Sono molto interessato a sapere chi lo sta facendo e perché.)
  • Evitali se vuoi sottoclassare gli builtin di lunghezza variabile come long, tuple o str e vuoi aggiungere degli attributi.
  • Evitali se insisti nel fornire valori predefiniti tramite gli attributi di classe per le variabili di istanza.

Potresti essere in grado di prendere in giro ulteriori avvertimenti dal resto della __slots__ documentazione ( i documenti di sviluppo 3.7 sono i più attuali) , a cui ho apportato significativi contributi recenti.

Critiche ad altre risposte

Le risposte principali attuali citano informazioni obsolete e sono abbastanza ondulate e mancano il segno in alcuni modi importanti.

Non "utilizzare solo __slots__quando si crea un'istanza di molti oggetti"

Quoto:

"Vorresti usare __slots__se hai intenzione di creare un'istanza di molti (centinaia, migliaia) oggetti della stessa classe."

Le classi base astratte, ad esempio, dal collectionsmodulo, non sono istanziate, ma __slots__sono dichiarate per esse.

Perché?

Se un utente desidera negare __dict__o __weakref__creare, tali elementi non devono essere disponibili nelle classi principali.

__slots__ contribuisce alla riusabilità durante la creazione di interfacce o mixin.

È vero che molti utenti di Python non stanno scrivendo per riusabilità, ma quando lo hai, avere la possibilità di negare l'utilizzo dello spazio non necessario è prezioso.

__slots__ non rompe il decapaggio

Quando decapaggio di un oggetto scanalato, è possibile che si lamenta di un fuorviante TypeError:

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

Questo non è corretto. Questo messaggio proviene dal protocollo più vecchio, che è quello predefinito. È possibile selezionare il protocollo più recente con l' -1argomento. In Python 2.7 questo sarebbe 2(che è stato introdotto in 2.3), e in 3.6 lo è 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>

Quindi vorrei tenerlo a mente, in quanto si tratta di un problema risolto.

Critica della risposta accettata (fino al 2 ottobre 2016)

Il primo paragrafo è metà spiegazione breve, metà predittiva. Ecco l'unica parte che risponde effettivamente alla domanda

L'uso corretto di __slots__è per risparmiare spazio negli oggetti. Invece di avere un dict dinamico che consente di aggiungere attributi agli oggetti in qualsiasi momento, esiste una struttura statica che non consente aggiunte dopo la creazione. Ciò consente di risparmiare l'overhead di un dict per ogni oggetto che utilizza gli slot

La seconda metà è un pio desiderio, e fuori dal segno:

Sebbene a volte si tratti di un'utile ottimizzazione, sarebbe del tutto superfluo se l'interprete Python fosse abbastanza dinamico da richiedere il comando solo in presenza di aggiunte all'oggetto.

Python in realtà fa qualcosa di simile a questo, creando solo __dict__quando si accede, ma creare molti oggetti senza dati è abbastanza ridicolo.

Il secondo paragrafo semplifica e manca i motivi reali da evitare __slots__. Il seguito non è un vero motivo per evitare gli slot (per motivi reali , vedi il resto della mia risposta sopra.):

Cambiano il comportamento degli oggetti che hanno slot in un modo che può essere abusato da maniaci del controllo e weenies di tipizzazione statici.

Quindi prosegue discutendo altri modi per raggiungere quell'obiettivo perverso con Python, senza discutere di nulla a che fare con __slots__ .

Il terzo paragrafo è un pensiero più ambizioso. Insieme è per lo più contenuto off-the-mark che il risponditore non ha nemmeno scritto e contribuisce a munizioni per i critici del sito.

Prova di utilizzo della memoria

Crea alcuni oggetti normali e oggetti scanalati:

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

Istanzia un milione di loro:

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

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

Accedi agli oggetti normali e ai loro __dict__e ispeziona di nuovo:

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

Ciò è coerente con la storia di Python, dai tipi e dalle classi Unifying in Python 2.2

Se si esegue la sottoclasse di un tipo incorporato, lo spazio aggiuntivo viene automaticamente aggiunto alle istanze per accogliere __dict__e __weakrefs__. (Non __dict__viene inizializzato fino a quando non lo si utilizza, quindi non dovresti preoccuparti dello spazio occupato da un dizionario vuoto per ogni istanza che crei.) Se non hai bisogno di questo spazio extra, puoi aggiungere la frase " __slots__ = []" a la tua classe.


14
caspita, una risposta incredibile - grazie! Tuttavia, non ho colto l' class Child(BaseA, BaseB): __slots__ = ('a', 'b')esempio con i genitori-empy-slot. Perché qui viene dictproxycreato invece di sollevare un AttributeErrorfor c?
Skandix,

@Skandix grazie per aver portato quel refuso alla mia attenzione, si è scoperto che non era un'istanza, probabilmente ho dimenticato che stavo modificando quella parte quando l'ho salvata nella cronologia dei post. Probabilmente sarebbe stato scoperto prima se avessi fatto la cosa giusta e avessi reso il codice più copiabile ... Grazie ancora!
Aaron Hall

38
Questa risposta dovrebbe far parte della documentazione ufficiale su Python __slots__. Sul serio! Grazie!
NightElfik,

13
@NightElfik che ci crediate o no, ho contribuito ai documenti di Python __slots__circa un anno fa: github.com/python/cpython/pull/1819/files
Aaron Hall

Risposta incredibilmente dettagliata. Ho una domanda: si dovrebbe usare gli slot come predefiniti a meno che l'uso non colpisca uno degli avvertimenti, o gli slot sono qualcosa da considerare se sai che stai lottando per la velocità / memoria? Per dirla in altro modo, dovresti incoraggiare un principiante a conoscerli e usarli fin dall'inizio?
Freethebees,

265

Citando Jacob Hallen :

L'uso corretto di __slots__è per risparmiare spazio negli oggetti. Invece di avere un dict dinamico che consente di aggiungere attributi agli oggetti in qualsiasi momento, esiste una struttura statica che non consente aggiunte dopo la creazione. [Questo uso di __slots__elimina il sovraccarico di un dict per ogni oggetto.] Sebbene a volte si tratti di un'utile ottimizzazione, sarebbe del tutto inutile se l'interprete di Python fosse abbastanza dinamico in modo da richiedere il dict solo quando c'erano effettivamente aggiunte al oggetto.

Sfortunatamente c'è un effetto collaterale negli slot. Cambiano il comportamento degli oggetti che hanno slot in un modo che può essere abusato da maniaci del controllo e weenies di tipizzazione statici. Questo è negativo, perché i maniaci del controllo dovrebbero abusare delle metaclasse e i weenies di tipizzazione statici dovrebbero abusare dei decoratori, poiché in Python dovrebbe esserci solo un modo ovvio di fare qualcosa.

Rendere CPython abbastanza intelligente da gestire il risparmio di spazio senza __slots__è una grande impresa, motivo per cui probabilmente non è nell'elenco delle modifiche per P3k (ancora).


86
Mi piacerebbe vedere qualche elaborazione sulla "tipizzazione statica" / punto del decoratore, senza peggiorativi. Non è utile citare terzi assenti. __slots__non affronta gli stessi problemi della digitazione statica. Ad esempio, in C ++, non si sta limitando la dichiarazione di una variabile membro, è l'assegnazione di un tipo non intenzionale (e il compilatore applicato) a quella variabile. Non sto perdonando l'uso di __slots__, solo interessato alla conversazione. Grazie!
Hiwaylon,

126

Dovresti usarlo __slots__se hai intenzione di creare un'istanza di molti (centinaia, migliaia) oggetti della stessa classe. __slots__esiste solo come strumento di ottimizzazione della memoria.

È altamente sconsigliato utilizzare __slots__per limitare la creazione di attributi.

Gli oggetti di decapaggio con __slots__non funzioneranno con il protocollo di decapaggio predefinito (più vecchio); è necessario specificare una versione successiva.

Alcune altre funzionalità di introspezione di Python possono anche essere influenzate negativamente.


10
Dimostro di decapare un oggetto scanalato nella mia risposta e anche di affrontare la prima parte della tua risposta.
Aaron Hall

2
Vedo il tuo punto, ma le slot offrono anche un accesso più veloce agli attributi (come altri hanno affermato). In tal caso non è necessario "istanziare molti (centinaia, migliaia) di oggetti della stessa classe" per ottenere prestazioni. Ciò di cui hai bisogno sono invece molti accessi allo stesso attributo (slot) della stessa istanza. (Per favore, correggimi se sbaglio.)
Rotareti,

61

Ogni oggetto python ha un __dict__attributo che è un dizionario che contiene tutti gli altri attributi. ad es. quando si digita self.attrpython sta effettivamente facendo self.__dict__['attr']. Come puoi immaginare, l'utilizzo di un dizionario per memorizzare l'attributo richiede dello spazio e del tempo extra per accedervi.

Tuttavia, quando si utilizza __slots__, qualsiasi oggetto creato per quella classe non avrà un __dict__attributo. Al contrario, l'accesso a tutti gli attributi avviene direttamente tramite puntatori.

Quindi, se si desidera una struttura in stile C piuttosto che una classe completa, è possibile utilizzare __slots__per compattare le dimensioni degli oggetti e ridurre il tempo di accesso agli attributi. Un buon esempio è una classe Point che contiene gli attributi x & y. Se hai molti punti, puoi provare a usare __slots__per conservare un po 'di memoria.


10
No, un'istanza di una classe con __slots__definita non è come una struttura in stile C. Esiste un dizionario a livello di classe che associa i nomi degli attributi agli indici, altrimenti non sarebbe possibile quanto segue: class A(object): __slots__= "value",\n\na=A(); setattr(a, 'value', 1)Penso davvero che questa risposta debba essere chiarita (posso farlo se vuoi). Inoltre, non sono sicuro che instance.__hidden_attributes[instance.__class__[attrname]]sia più veloce di instance.__dict__[attrname].
Tzot

22

Oltre alle altre risposte, ecco un esempio di utilizzo __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']

Quindi, per implementare __slots__, ci vuole solo una linea in più (e rendere la tua classe una nuova classe se non lo è già). In questo modo è possibile ridurre il footprint di memoria di quelle classi di 5 volte , a spese di dover scrivere un codice di decapaggio personalizzato, se e quando diventa necessario.


11

Gli slot sono molto utili per le chiamate in libreria per eliminare l '"invio del metodo denominato" quando si effettuano chiamate di funzione. Questo è menzionato nella documentazione SWIG . Per le librerie ad alte prestazioni che vogliono ridurre il sovraccarico di funzioni per le funzioni comunemente chiamate utilizzando gli slot è molto più veloce.

Ora questo potrebbe non essere direttamente correlato alla domanda dei PO. È più legato alla costruzione di estensioni che all'utilizzo della sintassi degli slot su un oggetto. Ma aiuta a completare il quadro per l'uso di slot e alcuni dei ragionamenti dietro di loro.


7

Un attributo di un'istanza di classe ha 3 proprietà: l'istanza, il nome dell'attributo e il valore dell'attributo.

Nel normale accesso agli attributi , l'istanza funge da dizionario e il nome dell'attributo funge da chiave in quel dizionario che cerca valore.

istanza (attributo) -> valore

In accesso __slots__ , il nome dell'attributo agisce come il dizionario e l'istanza si comporta come la chiave nel dizionario guardando in alto il valore.

attributo (istanza) -> valore

Nel modello flyweight , il nome dell'attributo funge da dizionario e il valore funge da chiave nel dizionario che cerca l'istanza.

attributo (valore) -> istanza


Questa è una buona condivisione e non si adatta bene a un commento su una delle risposte che suggeriscono anche pesi di mosca, ma non è una risposta completa alla domanda stessa. In particolare (solo nel contesto della domanda): perché Flyweight e "quali sono i casi che si dovrebbero evitare ..." __slots__?
Merlyn Morgan-Graham,

@Merlyn Morgan-Graham, serve come suggerimento per scegliere: accesso regolare, __slots__ o peso mosca.
Dmitry Rubanovich,

3

Un esempio molto semplice di __slot__attributo.

Problema: senza __slots__

Se non ho un __slot__attributo nella mia classe, posso aggiungere nuovi attributi ai miei oggetti.

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}

Se si guarda a esempio di cui sopra, si può vedere che obj1 e obj2 hanno un proprio x e y attributi e pitone ha anche creato un dictattributo per ogni oggetto ( obj1 e obj2 ).

Supponiamo che il mio test di classe abbia migliaia di tali oggetti? La creazione di un attributo aggiuntivo dictper ogni oggetto causerà un sacco di sovraccarico (memoria, potenza di calcolo ecc.) Nel mio codice.

Soluzione: con __slots__

Ora nell'esempio seguente la mia classe Test contiene l' __slots__attributo. Ora non posso aggiungere nuovi attributi ai miei oggetti (tranne l'attributo x) e python non crea più un dictattributo. Questo elimina il sovraccarico per ogni oggetto, che può diventare significativo se si hanno molti oggetti.

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

Un altro uso un po 'oscuro di __slots__è aggiungere attributi a un proxy oggetto dal pacchetto ProxyTypes, precedentemente parte del progetto PEAK. La sua ObjectWrappersi permette di procura un altro oggetto, ma intercetta tutte le interazioni con l'dell'oggetto proxy. Non è molto comunemente usato (e non supporta Python 3), ma l'abbiamo usato per implementare un wrapper di blocco thread-safe attorno a un'implementazione asincrona basata su tornado che rimbalza tutti gli accessi all'oggetto proxy attraverso l'ioloop, usando thread-safe concurrent.Futureoggetti per sincronizzare e restituire risultati.

Per impostazione predefinita, qualsiasi accesso all'attributo all'oggetto proxy ti darà il risultato dall'oggetto proxy. Se è necessario aggiungere un attributo sull'oggetto proxy, __slots__può essere utilizzato.

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

In sostanza, non serve a nulla __slots__.

Per il momento in cui si pensa che potrebbe essere necessario __slots__, in realtà si desidera utilizzare leggeri o peso mosca modelli di progettazione. Questi sono casi in cui non si desidera più utilizzare oggetti puramente Python. Invece, vuoi un wrapper simile ad un oggetto Python attorno ad un array, struct o numpy array.

class Flyweight(object):

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

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

Il wrapper di classe non ha attributi: fornisce solo metodi che agiscono sui dati sottostanti. I metodi possono essere ridotti ai metodi di classe. In effetti, potrebbe essere ridotto a sole funzioni che operano sull'array di dati sottostante.


17
Cosa c'entra Flyweight __slots__?
oefe

3
@oefe: certamente non capisco la tua domanda. Posso citare la mia risposta, se aiuta "quando pensi che potresti aver bisogno di slot , in realtà vuoi usare ... Modello di progettazione Flyweight". Questo è ciò che Flyweight ha a che fare con le slot . Hai una domanda più specifica?
S.Lott

21
@oefe: Flyweight e __slots__sono entrambe tecniche di ottimizzazione per risparmiare memoria. __slots__mostra i vantaggi quando si hanno molti molti oggetti e il modello di progettazione Flyweight. Entrambi risolvono lo stesso problema.
jfs,

7
Esiste un confronto disponibile tra l'utilizzo di slot e l'utilizzo di Flyweight per quanto riguarda il consumo di memoria e la velocità?
Kontulai,

8
Anche se Flyweight è sicuramente utile in alcuni contesti, che ci crediate o no, la risposta a "come posso ridurre l'uso della memoria in Python quando creo un milione di oggetti" non è sempre "non usare Python per i tuoi oggetti zillion". A volte __slots__è davvero la risposta, e come sottolinea Evgeni, può essere aggiunto come un semplice ripensamento (ad esempio, puoi prima concentrarti sulla correttezza, quindi aggiungere prestazioni).
Patrick Maupin,

0

La domanda originale riguardava casi d'uso generali non solo sulla memoria. Quindi, si dovrebbe menzionare qui che si ottengono anche prestazioni migliori quando si crea un'istanza di grandi quantità di oggetti - interessante ad esempio quando si analizzano documenti di grandi dimensioni in oggetti o da un database.

Ecco un confronto tra la creazione di alberi di oggetti con un milione di voci, l'utilizzo di slot e senza slot. Come riferimento anche le prestazioni quando si usano dicts semplici per gli alberi (Py2.7.10 su 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

Classi di prova (ident, appart da slot):

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, modalità dettagliata:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.