Utilisation de __slots__?

Réponses:

1020

En Python, à quoi sert __slots__et dans quels cas faut-il éviter cela?

TLDR:

L'attribut spécial __slots__vous permet d'indiquer explicitement les attributs d'instance que vous attendez de vos instances d'objet, avec les résultats attendus:

  1. accès aux attributs plus rapide .
  2. économie d'espace dans la mémoire.

L'économie d'espace est de

  1. Stockage des références de valeur dans des emplacements au lieu de __dict__.
  2. Refus __dict__et __weakref__création si les classes parentes les refusent et que vous déclarez __slots__.

Avertissements rapides

Petite mise en garde, vous ne devez déclarer un emplacement particulier qu'une seule fois dans une arborescence d'héritage. Par exemple:

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

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

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

Python ne s'oppose pas lorsque vous vous trompez (cela devrait probablement), les problèmes pourraient ne pas se manifester autrement, mais vos objets prendront plus d'espace qu'ils ne le devraient autrement. Python 3.8:

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

En effet, le descripteur d'emplacement de la base a un emplacement distinct de celui du mauvais. Cela ne devrait généralement pas se produire, mais cela pourrait:

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

La plus grande mise en garde concerne l'héritage multiple - plusieurs "classes parentes avec des emplacements non vides" ne peuvent pas être combinées.

Pour tenir compte de cette restriction, suivez les meilleures pratiques: factorisez toutes les abstractions sauf un ou tous les parents dont leur classe concrète respectivement et votre nouvelle classe concrète hériteront collectivement - en donnant aux abstractions des emplacements vides (tout comme les classes de base abstraites dans le bibliothèque standard).

Voir la section sur l'héritage multiple ci-dessous pour un exemple.

Exigences:

  • Pour que les attributs nommés __slots__soient réellement stockés dans des emplacements au lieu de a __dict__, une classe doit hériter de object.

  • Pour empêcher la création d'un __dict__, vous devez hériter de objectet toutes les classes de l'héritage doivent déclarer __slots__et aucune d'entre elles ne peut avoir d' '__dict__'entrée.

Il y a beaucoup de détails si vous souhaitez continuer à lire.

Pourquoi utiliser __slots__: Accès plus rapide aux attributs.

Le créateur de Python, Guido van Rossum, déclare qu'il a réellement créé __slots__pour un accès aux attributs plus rapide.

Il est trivial de démontrer un accès plus rapide et mesurable:

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

et

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

L'accès fendu est presque 30% plus rapide en Python 3.5 sur Ubuntu.

>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342

Dans Python 2 sur Windows, je l'ai mesuré environ 15% plus rapidement.

Pourquoi utiliser __slots__: Économies de mémoire

Un autre objectif de __slots__est de réduire l'espace en mémoire occupé par chaque instance d'objet.

Ma propre contribution à la documentation indique clairement les raisons de cela :

L'espace économisé sur l'utilisation __dict__peut être important.

SQLAlchemy attribue de nombreuses économies de mémoire à __slots__.

Pour vérifier cela, en utilisant la distribution Anaconda de Python 2.7 sur Ubuntu Linux, avec guppy.hpy(alias heapy) et sys.getsizeof, la taille d'une instance de classe sans __slots__déclaré, et rien d'autre, est de 64 octets. Cela n'inclut pas le __dict__. Merci encore Python pour l'évaluation paresseuse, le __dict__n'est apparemment pas appelé jusqu'à ce qu'il soit référencé, mais les classes sans données sont généralement inutiles. Lorsqu'il est appelé, cet __dict__attribut comporte en outre au moins 280 octets.

En revanche, une instance de classe avec __slots__déclarée être ()(pas de données) ne fait que 16 octets, et 56 octets au total avec un élément dans les emplacements, 64 avec deux.

Pour Python 64 bits, j'illustre la consommation de mémoire en octets en Python 2.7 et 3.6, pour __slots__et __dict__(aucun emplacement défini) pour chaque point où le dict croît en 3.6 (sauf pour les attributs 0, 1 et 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

Donc, en dépit de petits dict dans Python 3, nous voyons à quel point __slots__les instances sont bien adaptées pour nous économiser de la mémoire, et c'est une raison majeure que vous voudriez utiliser __slots__.

Juste pour l'intégralité de mes notes, notez qu'il y a un coût unique par emplacement dans l'espace de noms de la classe de 64 octets en Python 2 et 72 octets en Python 3, car les emplacements utilisent des descripteurs de données comme des propriétés, appelés "membres".

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

Démonstration de __slots__:

Pour refuser la création d'un __dict__, vous devez sous object- classer :

class Base(object): 
    __slots__ = ()

maintenant:

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

Ou sous-classe une autre classe qui définit __slots__

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

et maintenant:

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

mais:

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

Pour permettre la __dict__création tout en sous-classant les objets fendus, ajoutez simplement '__dict__'à __slots__(notez que les emplacements sont ordonnés, et vous ne devez pas répéter les emplacements qui sont déjà dans les classes parentes):

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

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

et

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

Ou vous n'avez même pas besoin de déclarer __slots__dans votre sous-classe, et vous utiliserez toujours des emplacements des parents, mais ne restreignez pas la création d'un __dict__:

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

Et:

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

Cependant, cela __slots__peut entraîner des problèmes d'héritage multiple:

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

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

Parce que la création d'une classe enfant à partir de parents avec les deux emplacements non vides échoue:

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

Si vous rencontrez ce problème, vous pouvez simplement supprimer __slots__des parents, ou si vous avez le contrôle des parents, leur donner des emplacements vides ou refactoriser les abstractions:

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!

Ajouter '__dict__'à __slots__pour obtenir une affectation dynamique:

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

et maintenant:

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

Donc, avec les '__dict__'slots, nous perdons certains des avantages de taille avec l'avantage d'avoir une affectation dynamique et d'avoir toujours des slots pour les noms que nous attendons.

Lorsque vous héritez d'un objet qui n'est pas fendu, vous obtenez le même type de sémantique lorsque vous utilisez __slots__- des noms qui __slots__pointent vers des valeurs fendues, tandis que toutes les autres valeurs sont placées dans l'instance __dict__.

Éviter __slots__parce que vous voulez être en mesure d'ajouter des attributs à la volée n'est en fait pas une bonne raison - ajoutez-le simplement si "__dict__"vous en avez __slots__besoin.

Vous pouvez également ajouter __weakref__à __slots__explicitement si vous avez besoin de cette fonctionnalité.

Définissez sur vide tuple lors de la sous-classe d'un tuple nommé:

Le builtin namedtuple crée des instances immuables qui sont très légères (essentiellement, la taille des tuples) mais pour obtenir les avantages, vous devez le faire vous-même si vous les sous-classe:

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

usage:

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

Et essayer d'attribuer un attribut inattendu soulève un AttributeErrorcar nous avons empêché la création de __dict__:

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

Vous pouvez autoriser la __dict__création en laissant de côté __slots__ = (), mais vous ne pouvez pas utiliser non vide __slots__avec des sous - types de tuple.

Plus grande mise en garde: héritage multiple

Même lorsque les emplacements non vides sont identiques pour plusieurs parents, ils ne peuvent pas être utilisés ensemble:

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'utilisation d'un vide __slots__dans le parent semble offrir la plus grande flexibilité, permettant à l'enfant de choisir d'empêcher ou d'autoriser (en ajoutant '__dict__'pour obtenir une affectation dynamique, voir la section ci-dessus) la création d'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'

Vous n'avez pas besoin d'avoir des emplacements - donc si vous les ajoutez et les supprimez plus tard, cela ne devrait pas poser de problème.

Sortir sur une branche ici : Si vous composez des mixins ou utilisez des classes de base abstraites , qui ne sont pas destinées à être instanciées, un vide __slots__dans ces parents semble être la meilleure voie à suivre en termes de flexibilité pour les sous-classes.

Pour démontrer, tout d'abord, créons une classe avec du code que nous aimerions utiliser sous héritage multiple

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

Nous pourrions utiliser ce qui précède directement en héritant et en déclarant les emplacements attendus:

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

Mais cela nous importe peu, c'est un héritage simple trivial, nous avons besoin d'une autre classe dont nous pourrions également hériter, peut-être avec un attribut bruyant:

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

Maintenant, si les deux bases avaient des emplacements non vides, nous ne pourrions pas faire ce qui suit. (En fait, si nous le voulions, nous aurions pu donner des AbstractBaseemplacements non vides a et b, et les laisser en dehors de la déclaration ci-dessous - les laisser dedans serait faux):

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

Et maintenant, nous avons des fonctionnalités des deux via l'héritage multiple, et nous pouvons toujours refuser __dict__et __weakref__instancier:

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

Autres cas pour éviter les créneaux horaires:

  • Évitez-les lorsque vous souhaitez effectuer une __class__affectation avec une autre classe qui ne les a pas (et vous ne pouvez pas les ajouter) sauf si les dispositions des emplacements sont identiques. (Je suis très intéressé à savoir qui fait cela et pourquoi.)
  • Évitez-les si vous souhaitez sous-classer des commandes de longueur variable comme long, tuple ou str, et que vous souhaitez leur ajouter des attributs.
  • Évitez-les si vous insistez pour fournir des valeurs par défaut via des attributs de classe pour les variables d'instance.

Vous pourrez peut-être mettre en évidence d'autres mises en garde du reste de la __slots__ documentation ( les documents de développement 3.7 sont les plus récents) , auxquels j'ai apporté d'importantes contributions récentes.

Critiques d'autres réponses

Les meilleures réponses actuelles citent des informations obsolètes et sont assez ondulées à la main et manquent la marque de certaines manières importantes.

Ne pas "utiliser uniquement __slots__lors de l'instanciation de nombreux objets"

Je cite:

"Vous voudriez utiliser __slots__si vous allez instancier beaucoup (des centaines, des milliers) d'objets de la même classe."

Les classes de base abstraites, par exemple, du collectionsmodule, ne sont pas instanciées, mais __slots__sont déclarées pour elles.

Pourquoi?

Si un utilisateur souhaite refuser __dict__ou __weakref__créer, ces éléments ne doivent pas être disponibles dans les classes parentes.

__slots__ contribue à la réutilisabilité lors de la création d'interfaces ou de mixins.

Il est vrai que de nombreux utilisateurs de Python n'écrivent pas pour la réutilisation, mais lorsque vous l'êtes, avoir la possibilité de refuser l'utilisation inutile de l'espace est précieux.

__slots__ ne casse pas le décapage

Lorsque vous décapez un objet à fente, vous pouvez trouver qu'il se plaint d'un trompeur TypeError:

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

C'est en fait incorrect. Ce message provient du protocole le plus ancien, qui est le protocole par défaut. Vous pouvez sélectionner le dernier protocole avec l' -1argument. En Python 2.7, ce serait 2(qui a été introduit en 2.3), et en 3.6 c'est le cas 4.

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

en Python 2.7:

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

en Python 3.6

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

Je garderais donc cela à l'esprit, car c'est un problème résolu.

Critique de la réponse acceptée (jusqu'au 2 octobre 2016)

Le premier paragraphe est moitié explication courte, moitié prédictive. Voici la seule partie qui répond réellement à la question

La bonne utilisation de __slots__est d'économiser de l'espace dans les objets. Au lieu d'avoir un dict dynamique qui permet d'ajouter des attributs aux objets à tout moment, il existe une structure statique qui n'autorise pas les ajouts après la création. Cela économise les frais généraux d'un dict pour chaque objet qui utilise des emplacements

La seconde moitié est un vœu pieux, et hors de la marque:

Bien que cela soit parfois une optimisation utile, il serait complètement inutile si l'interpréteur Python était suffisamment dynamique pour qu'il ne nécessite la dictée que lorsqu'il y avait effectivement des ajouts à l'objet.

Python fait en fait quelque chose de similaire à cela, créant uniquement le __dict__quand il est accédé, mais créer beaucoup d'objets sans données est assez ridicule.

Le deuxième paragraphe simplifie à l'excès et manque des raisons réelles à éviter __slots__. Ce qui suit n'est pas une vraie raison pour éviter les créneaux horaires (pour des raisons réelles , voir le reste de ma réponse ci-dessus.):

Ils changent le comportement des objets qui ont des emplacements d'une manière qui peut être abusée par des monstres de contrôle et des caractères de frappe statiques.

Il continue ensuite à discuter d'autres façons d'accomplir cet objectif pervers avec Python, sans discuter de quoi que ce soit à voir avec __slots__.

Le troisième paragraphe est un vœu pieux. Ensemble, il s'agit principalement de contenu hors du commun que le répondeur n'a même pas rédigé et contribue à des munitions pour les critiques du site.

Preuve d'utilisation de la mémoire

Créez des objets normaux et des objets fendus:

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

Instanciez un million d'entre eux:

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

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

Accédez aux objets réguliers et à leur __dict__et inspectez à nouveau:

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

Ceci est cohérent avec l'histoire de Python, à partir des types et classes Unifying dans Python 2.2

Si vous sous-classe un type intégré, un espace supplémentaire est automatiquement ajouté aux instances pour accueillir __dict__et __weakrefs__. (Le __dict__n'est pas initialisé tant que vous ne l'utilisez pas, vous ne devez donc pas vous soucier de l'espace occupé par un dictionnaire vide pour chaque instance que vous créez.) Si vous n'avez pas besoin de cet espace supplémentaire, vous pouvez ajouter la phrase " __slots__ = []" à ta classe.

Aaron Hall
la source
14
wow, une sacrée réponse - merci! Cependant, je n'ai pas pris l' class Child(BaseA, BaseB): __slots__ = ('a', 'b')exemple avec les parents empy-slot. Pourquoi est-il dictproxycréé ici au lieu de lever un AttributeErrorpour c?
Skandix
@Skandix merci d'avoir porté cette faute de frappe à mon attention, il s'est avéré que ce n'était pas une instanciation, j'ai probablement oublié que je modifiais cette partie quand je l'ai enregistrée dans l'historique de la publication. Il aurait probablement été rattrapé plus tôt si j'avais fait la bonne chose et rendu le code plus copiable-collé ... Merci encore!
Aaron Hall
38
Cette réponse devrait faire partie de la documentation officielle de Python sur __slots__. Sérieusement! Je vous remercie!
NightElfik
13
@NightElfik croyez-le ou non, j'ai contribué aux documents Python il y a __slots__environ un an: github.com/python/cpython/pull/1819/files
Aaron Hall
Réponse incroyablement détaillée. J'ai une question: faut-il utiliser les emplacements par défaut, sauf si l'utilisation atteint l'une des mises en garde, ou les emplacements sont-ils quelque chose à considérer si vous savez que vous allez avoir du mal pour la vitesse / mémoire? En d'autres termes, devriez-vous encourager un débutant à en savoir plus sur eux et à les utiliser dès le départ?
freethebees
265

Citant Jacob Hallen :

La bonne utilisation de __slots__est d'économiser de l'espace dans les objets. Au lieu d'avoir un dict dynamique qui permet d'ajouter des attributs aux objets à tout moment, il existe une structure statique qui n'autorise pas les ajouts après la création. [Cette utilisation de __slots__élimine les frais généraux d'un dict pour chaque objet.] Bien qu'il s'agisse parfois d'une optimisation utile, il serait complètement inutile si l'interpréteur Python était suffisamment dynamique pour ne nécessiter le dict que lorsqu'il y avait effectivement des ajouts au objet.

Malheureusement, les machines à sous ont un effet secondaire. Ils changent le comportement des objets qui ont des emplacements d'une manière qui peut être abusée par des monstres de contrôle et des caractères de frappe statiques. C'est mauvais, car les monstres de contrôle devraient abuser des métaclasses et les caractères de frappe statiques devraient abuser des décorateurs, car en Python, il ne devrait y avoir qu'une seule façon évidente de faire quelque chose.

Rendre CPython assez intelligent pour gérer l'espace sans, __slots__est une entreprise majeure, c'est probablement pourquoi il n'est pas (encore) sur la liste des changements pour P3k.

Jeff Bauer
la source
86
J'aimerais voir quelques détails sur le point "typage statique" / décorateur, sans péjoratifs. Citer des tiers absents est inutile. __slots__ne résout pas les mêmes problèmes que la frappe statique. Par exemple, en C ++, ce n'est pas la déclaration d'une variable membre qui est restreinte, c'est l'affectation d'un type involontaire (et d'un compilateur imposé) à cette variable. Je n'approuve pas l'utilisation de __slots__, juste intéressé par la conversation. Merci!
hiwaylon
126

Vous voudrez utiliser __slots__si vous allez instancier beaucoup (des centaines, des milliers) d'objets de la même classe. __slots__n'existe que comme outil d'optimisation de la mémoire.

Il est fortement déconseillé d'utiliser __slots__pour limiter la création d'attributs.

Le décapage d'objets avec __slots__ne fonctionnera pas avec le protocole de décapage par défaut (le plus ancien); il est nécessaire de spécifier une version ultérieure.

Certaines autres fonctionnalités d'introspection de python peuvent également être affectées.

Ryan
la source
10
Je démontre le décapage d'un objet fendu dans ma réponse et aborde également la première partie de votre réponse.
Aaron Hall
2
Je vois votre point, mais les machines à sous offrent également un accès aux attributs plus rapide (comme d'autres l'ont déclaré). Dans ce cas, vous n'avez pas besoin "d'instancier beaucoup (des centaines, des milliers) d'objets de la même classe" pour gagner en performances. Ce dont vous avez besoin à la place, c'est de nombreux accès au même attribut (fendu) de la même instance. (Veuillez me corriger si je me trompe.)
Rotareti
61

Chaque objet python a un __dict__attribut qui est un dictionnaire contenant tous les autres attributs. par exemple, lorsque vous tapez self.attrpython est en train de faire self.__dict__['attr']. Comme vous pouvez l'imaginer, l'utilisation d'un dictionnaire pour stocker l'attribut prend un peu plus de temps et d'espace pour y accéder.

Cependant, lorsque vous utilisez __slots__, tout objet créé pour cette classe n'aura pas d' __dict__attribut. Au lieu de cela, tout accès aux attributs se fait directement via des pointeurs.

Donc, si vous voulez une structure de style C plutôt qu'une classe à part entière, vous pouvez utiliser __slots__pour compacter la taille des objets et réduire le temps d'accès aux attributs. Un bon exemple est une classe Point contenant les attributs x & y. Si vous allez avoir beaucoup de points, vous pouvez essayer de les utiliser __slots__pour économiser de la mémoire.

Suraj
la source
10
Non, une instance d'une classe avec __slots__défini n'est pas comme une structure de style C. Il existe un dictionnaire de niveau classe mappant les noms d'attribut aux index, sinon ce ne serait pas possible: class A(object): __slots__= "value",\n\na=A(); setattr(a, 'value', 1)Je pense vraiment que cette réponse devrait être clarifiée (je peux le faire si vous le souhaitez). De plus, je ne suis pas certain que ce instance.__hidden_attributes[instance.__class__[attrname]]soit plus rapide que instance.__dict__[attrname].
tzot
22

En plus des autres réponses, voici un exemple d'utilisation __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']

Donc, pour l'implémenter __slots__, cela ne prend qu'une ligne supplémentaire (et faire de votre classe une classe de nouveau style si ce n'est pas déjà fait). De cette façon, vous pouvez réduire l'empreinte mémoire de ces classes de 5 fois , au détriment d'avoir à écrire du code de pickle personnalisé, si et quand cela devient nécessaire.

Evgeni Sergeev
la source
11

Les emplacements sont très utiles pour les appels de bibliothèque pour éliminer la «répartition de méthode nommée» lors des appels de fonction. Ceci est mentionné dans la documentation SWIG . Pour les bibliothèques hautes performances qui souhaitent réduire la surcharge de fonctions pour les fonctions communément appelées utilisant des slots, c'est beaucoup plus rapide.

Maintenant, cela peut ne pas être directement lié à la question des PO. Elle est plus liée à la création d'extensions qu'à l'utilisation de la syntaxe des emplacements sur un objet. Mais cela aide à compléter l'image de l'utilisation des créneaux horaires et certains des motifs derrière eux.

Demolishun
la source
7

Un attribut d'une instance de classe a 3 propriétés: l'instance, le nom de l'attribut et la valeur de l'attribut.

Dans l' accès normal aux attributs , l'instance agit comme un dictionnaire et le nom de l'attribut agit comme la clé de ce dictionnaire en recherchant la valeur.

instance (attribut) -> valeur

Dans l' accès __slots__ , le nom de l'attribut agit comme le dictionnaire et l'instance agit comme la clé dans la valeur de recherche du dictionnaire.

attribut (instance) -> valeur

Dans le modèle flyweight , le nom de l'attribut agit comme le dictionnaire et la valeur agit comme la clé dans ce dictionnaire qui recherche l'instance.

attribut (valeur) -> instance

Dmitry Rubanovich
la source
C'est une bonne part, et ne rentrera pas bien dans un commentaire sur l'une des réponses qui suggèrent également des poids volants, mais ce n'est pas une réponse complète à la question elle-même. En particulier (dans le contexte juste de la question): pourquoi Flyweight, et "quels sont les cas à éviter ..." __slots__?
Merlyn Morgan-Graham
@Merlyn Morgan-Graham, il sert d'indice sur lequel choisir: accès régulier, __slots__ ou masselotte.
Dmitry Rubanovich
3

Un exemple très simple d' __slot__attribut.

Problème: Sans __slots__

Si je n'ai pas d' __slot__attribut dans ma classe, je peux ajouter de nouveaux attributs à mes objets.

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}

Si vous regardez l'exemple ci-dessus, vous pouvez voir que obj1 et obj2 ont leurs propres attributs x et y et python a également créé un dictattribut pour chaque objet ( obj1 et obj2 ).

Supposons que ma classe Test ait des milliers de ces objets? La création d'un attribut supplémentaire dictpour chaque objet entraînera beaucoup de surcharge (mémoire, puissance de calcul, etc.) dans mon code.

Solution: avec __slots__

Maintenant, dans l'exemple suivant, ma classe Test contient un __slots__attribut. Maintenant, je ne peux plus ajouter de nouveaux attributs à mes objets (sauf l'attribut x) et python ne crée dictplus d'attribut. Cela élimine la surcharge pour chaque objet, ce qui peut devenir important si vous avez plusieurs objets.

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
la source
2

Une autre utilisation quelque peu obscure de __slots__est d'ajouter des attributs à un proxy d'objet à partir du package ProxyTypes, qui faisait auparavant partie du projet PEAK. Son ObjectWrappervous permet de proxy un autre objet, mais d' intercepter toutes les interactions avec l'objet proxy. Il n'est pas très utilisé (et pas de prise en charge de Python 3), mais nous l'avons utilisé pour implémenter un wrapper de blocage thread-safe autour d'une implémentation asynchrone basée sur tornado qui fait rebondir tout accès à l'objet proxy via l'ioloop, en utilisant thread-safeconcurrent.Future objets pour synchroniser et renvoyer les résultats.

Par défaut, tout accès d'attribut à l'objet proxy vous donnera le résultat de l'objet proxy. Si vous devez ajouter un attribut sur l'objet proxy, __slots__peut être utilisé.

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
la source
1

Vous n'avez - essentiellement - aucune utilité pour __slots__.

Pour le moment où vous pensez en avoir besoin __slots__, vous voulez réellement utiliser des modèles de conception légers ou mouches . Ce sont des cas où vous ne souhaitez plus utiliser des objets purement Python. Au lieu de cela, vous voulez un wrapper de type objet Python autour d'un tableau, d'une structure ou d'un tableau numpy.

class Flyweight(object):

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

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

L'encapsuleur de classe n'a pas d'attributs - il fournit simplement des méthodes qui agissent sur les données sous-jacentes. Les méthodes peuvent être réduites à des méthodes de classe. En effet, il pourrait être réduit à de simples fonctions fonctionnant sur le tableau de données sous-jacent.

S.Lott
la source
17
Qu'est-ce que Flyweight a à voir avec __slots__?
oefe
3
@oefe: Je ne comprends certainement pas votre question. Je peux citer ma réponse, si cela aide "quand vous pensez que vous pourriez avoir besoin de fentes , vous voulez réellement utiliser ... le modèle de conception Flyweight". C'est ce que Flyweight a à voir avec les machines à sous . Vous avez une question plus précise?
S.Lott
21
@oefe: Flyweight et __slots__sont deux techniques d'optimisation pour économiser de la mémoire. __slots__présente des avantages lorsque vous disposez de nombreux objets ainsi que d'un motif de conception Flyweight. Les deux résolvent le même problème.
jfs
7
Existe-t-il une comparaison entre l'utilisation des slots et l'utilisation de Flyweight en ce qui concerne la consommation de mémoire et la vitesse?
kontulai
8
Bien que Flyweight soit certainement utile dans certains contextes, croyez-le ou non, la réponse à "comment puis-je réduire l'utilisation de la mémoire en Python lorsque je crée des objets zillion" n'est pas toujours "n'utilisez pas Python pour vos objets zillion." Parfois, c'est __slots__vraiment la réponse, et comme Evgeni le fait remarquer, il peut être ajouté comme une simple réflexion après coup (par exemple, vous pouvez vous concentrer sur l'exactitude, puis ajouter des performances).
Patrick Maupin
0

La question initiale portait sur les cas d'utilisation générale, pas seulement sur la mémoire. Il convient donc de mentionner ici que vous obtenez également de meilleures performances lors de l'instanciation de grandes quantités d'objets, ce qui est intéressant, par exemple, lors de l'analyse de gros documents en objets ou à partir d'une base de données.

Voici une comparaison de la création d'arbres d'objets avec un million d'entrées, en utilisant des emplacements et sans emplacements. Comme référence également les performances lors de l'utilisation de dict simples pour les arbres (Py2.7.10 sur 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

Classes de test (ident, en dehors des 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)

code de test, mode détaillé:

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
Pilule rouge
la source