Quel est le but de __slots__
dans Python - en particulier en ce qui concerne le moment où je souhaiterais l'utiliser et le moment non?
En Python, quel est le but de
__slots__
et quels sont les cas à éviter?
L'attribut spécial __slots__
vous permet d'indiquer explicitement quels attributs d'instance vous attendez de vos instances d'objet, avec les résultats attendus:
Les économies d'espace sont de
__dict__
.__dict__
et __weakref__
si les classes parent les refusent et que vous déclariez __slots__
.Petit bémol, vous ne devez déclarer une case particulière qu'une fois dans un arbre 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 fait pas d'objection lorsque vous vous trompez (cela devrait probablement), les problèmes pourraient ne pas se manifester autrement, mais vos objets prendront plus de place qu'ils ne le devraient autrement.
>>> from sys import getsizeof
>>> getsizeof(Right()), getsizeof(Wrong())
(64, 80)
La plus grande mise en garde concerne l'héritage multiple: il est impossible de combiner plusieurs "classes parent avec des emplacements non vides".
Pour respecter cette restriction, suivez les meilleures pratiques: Découpez l’abstraction de tous les parents sauf un ou tous ceux dont leur classe concrète et leur nouvelle classe concrète hériteront collectivement - en donnant à la/les abstraction (s) des emplacements vides (tout comme les classes de base abstraites de bibliothèque standard).
Voir la section sur l'héritage multiple ci-dessous pour un exemple.
Pour que les attributs nommés dans __slots__
soient réellement stockés dans des emplacements au lieu d'un __dict__
, une classe doit hériter de object
.
Pour empêcher la création d'un __dict__
, vous devez hériter de object
et toutes les classes de l'héritage doivent déclarer __slots__
et aucune d'entre elles ne peut avoir une entrée '__dict__'
.
Il y a beaucoup de détails si vous souhaitez continuer à lire.
__slots__
: Accès d'attribut plus rapide.Le créateur de Python, Guido van Rossum, indique qu'il a en fait créé __slots__
pour un accès plus rapide aux attributs.
Il est trivial de démontrer un accès sensiblement plus rapide:
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 par créneau est presque 30% plus rapide sous Python 3.5 sous Ubuntu.
>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342
Dans Python 2 sous Windows, je l’ai mesuré environ 15% plus vite.
__slots__
: Economie de mémoireUn autre objectif de __slots__
est de réduire l'espace mémoire occupé par chaque instance d'objet.
Ma propre contribution à la documentation énonce clairement les raisons derrière cela :
L'espace économisé sur
__dict__
peut être important.
attributs SQLAlchemy beaucoup d'économies de mémoire pour __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 Python pour une nouvelle évaluation, la __dict__
n'est apparemment pas appelée jusqu'à ce qu'elle soit référencée, mais les classes sans données sont généralement inutiles. Lorsqu'il est appelé, l'existence de l'attribut __dict__
est de 280 octets minimum.
En revanche, une instance de classe avec __slots__
déclarée comme étant ()
(aucune donnée) ne représente 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 dans Python 2.7 et 3.6, pour __slots__
et __dict__
(aucun emplacement défini) pour chaque point où le dict se développe 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
Ainsi, malgré de plus petites tailles dans Python 3, nous voyons comment __slots__
redimensionne joliment pour que les instances nous épargnent de la mémoire. C’est une raison majeure pour laquelle vous voudriez utiliser __slots__
.
Pour que mes notes soient complètes, notez qu'il existe un coût unique par emplacement dans l'espace de noms de la classe de 64 octets dans Python 2 et de 72 octets dans Python 3, car utiliser des descripteurs de données tels que des propriétés, appelés "membres".
>>> Foo.foo
<member 'foo' of 'Foo' objects>
>>> type(Foo.foo)
<class 'member_descriptor'>
>>> getsizeof(Foo.foo)
72
__slots__
:Pour refuser la création d'un __dict__
, vous devez placer la sous-classe object
:
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 autoriser la création de __dict__
lors de la sous-classification d'objets à créneaux, ajoutez simplement '__dict__'
au __slots__
(notez que les emplacements sont ordonnés et que vous ne devez pas répéter les emplacements 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 les slots des parents, sans toutefois restreindre la création d'un __dict__
:
class NoSlots(Child): pass
ns = NoSlots()
ns.a = 'a'
ns.b = 'b'
Et:
>>> ns.__dict__
{'b': 'b'}
Cependant, __slots__
peut entraîner des problèmes pour l'héritage multiple:
class BaseA(object):
__slots__ = ('a',)
class BaseB(object):
__slots__ = ('b',)
La création d'une classe enfant à partir des parents avec les deux emplacements non vides échouant:
>>> 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 pourriez simplement supprimer __slots__
des parents, ou, si vous avez le contrôle sur les parents, leur attribuer des espaces vides, ou refactoriser des 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!
'__dict__'
à __slots__
pour obtenir une affectation dynamique:class Foo(object):
__slots__ = 'bar', 'baz', '__dict__'
et maintenant:
>>> foo = Foo()
>>> foo.boink = 'boink'
Donc, avec '__dict__'
dans les slots, nous perdons certains des avantages de taille avec l'avantage d'avoir une affectation dynamique et d'avoir toujours des emplacements pour les noms que nous attendons.
Lorsque vous héritez d'un objet qui n'a pas été placé, vous obtenez le même type de sémantique lorsque vous utilisez __slots__
- les noms qui se trouvent dans __slots__
pointent sur des valeurs définies, tandis que les autres valeurs instance de __dict__
.
Éviter __slots__
parce que vous souhaitez pouvoir ajouter des attributs à la volée n’est en fait pas une bonne raison. Ajoutez simplement "__dict__"
à votre __slots__
si cela est nécessaire.
De même, vous pouvez ajouter __weakref__
à __slots__
explicitement si vous avez besoin de cette fonctionnalité.
La commande namedtuple crée des instances immuables très légères (essentiellement la taille des n-uplets), mais pour obtenir les avantages, vous devez le faire vous-même si vous les sous-classez:
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 déclenche une AttributeError
car 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 création de __dict__
en laissant __slots__ = ()
, mais vous ne pouvez pas utiliser __slots__
non vide avec des sous-types de Tuple.
Même lorsque les créneaux horaires 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
Utiliser un __slots__
vide dans le parent semble offrir la plus grande souplesse 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 pour avoir des emplacements - donc si vous les ajoutez et les supprimez plus tard, cela ne devrait poser aucun problème.
Aller sur une branche ici : Si vous composez mixins ou utilisez classes de base abstraites , qui ne sont pas destinés à être instanciés, un __slots__
vide chez ces parents semble être la meilleure voie à suivre en termes de flexibilité pour les sous-classes.
Pour démontrer, commençons par créer une classe avec le 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 créneaux attendus:
class Foo(AbstractBase):
__slots__ = 'a', 'b'
Mais nous ne nous soucions pas de ça, c'est de l'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 slots non vides, nous ne pourrions pas faire le dessous. (En fait, si nous le voulions, nous aurions pu donner AbstractBase
logements non vides a et b, et les laisser en dehors de la déclaration ci-dessous - les laisser entrer aurait tort.
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 l'instance __dict__
et __weakref__
:
>>> c = Concretion('a', 'b')
>>> c.c = c
setting c!
>>> c.c
getting c!
Concretion('a', 'b')
>>> c.d = 'd'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Concretion' object has no attribute 'd'
__class__
avec une autre classe qui n'en a pas (et vous ne pouvez pas les ajouter) à moins que les dispositions des emplacements ne soient identiques. (Je suis très intéressé d'apprendre qui fait cela et pourquoi.)Vous pourrez peut-être écarter d'autres mises en garde du reste du __slots__
documentation (les documents de développement 3.7 sont les plus récents) , auxquels j'ai récemment apporté d'importantes contributions.
Les principales réponses actuelles citent des informations obsolètes, sont plutôt vagues et manquent à certains égards.
__slots__
pour instancier beaucoup d'objets"Je cite:
"Vous voudriez utiliser
__slots__
si vous voulez instancier beaucoup (des centaines, des milliers) d'objets de la même classe."
Les classes de base abstraites, par exemple, du module collections
, ne sont pas instanciées, mais __slots__
est déclaré pour elles.
Pourquoi?
Si un utilisateur souhaite refuser la création de __dict__
ou __weakref__
, ces éléments ne doivent pas être disponibles dans les classes parentes.
__slots__
contribue à la réutilisation lors de la création d'interfaces ou de mixins.
Il est vrai que de nombreux utilisateurs Python n'écrivent pas à des fins de réutilisation, mais le cas échéant, l'option de refuser l'utilisation d'espace inutile est utile.
__slots__
n'interrompt pas le décapageLorsque vous choisissez un objet fendu, vous pouvez constater qu'il se plaint d'un TypeError
trompeur:
>>> pickle.loads(pickle.dumps(f))
TypeError: a class that defines __slots__ without defining __getstate__ cannot be pickled
Ceci est en fait incorrect. Ce message provient du protocole le plus ancien, le protocole par défaut. Vous pouvez sélectionner le dernier protocole avec l'argument -1
. Dans Python2.7, il s'agirait de 2
(introduit dans la version 2.3), et en 3.6, de 4
.
>>> pickle.loads(pickle.dumps(f, -1))
<__main__.Foo object at 0x1129C770>
dans Python 2.7:
>>> pickle.loads(pickle.dumps(f, 2))
<__main__.Foo object at 0x1129C770>
dans Python 3.6
>>> pickle.loads(pickle.dumps(f, 4))
<__main__.Foo object at 0x1129C770>
Donc, je garderais cela à l'esprit, car c'est un problème résolu.
Le premier paragraphe est une explication moitié courte, moitié prédictive. Voici la seule partie qui répond réellement à la question
L'utilisation correcte de
__slots__
consiste à é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 évite la surcharge d’un dict pour chaque objet utilisant des slots
La seconde moitié est un vœu pieux, et off the mark:
Bien que cette optimisation soit parfois utile, il serait tout à fait inutile que l’interpréteur Python soit suffisamment dynamique pour ne nécessiter le dict que lorsqu’il y a eu des ajouts à l’objet.
Python fait quelque chose de similaire à cela, ne créant que le __dict__
lorsqu’on y accède, mais créer beaucoup d’objets sans données est assez ridicule.
Le deuxième paragraphe simplifie à l'excès et omet les raisons réelles d'éviter __slots__
. Le texte ci-dessous est et non une véritable raison d'éviter les créneaux (pour les raisons réelles , voir le reste de ma réponse ci-dessus.):
Ils modifient le comportement des objets dotés d'emplacements de manière à ce qu'ils puissent être utilisés de manière abusive par les maniaques de contrôle et les marqueurs statiques.
Il poursuit ensuite en discutant d’autres moyens d’atteindre cet objectif pervers avec Python, sans discuter de tout ce qui a trait à __slots__
.
Le troisième paragraphe est un vœu pieux. Ensemble, il s’agit pour la plupart de contenu hors propos que le répondeur n’a même pas écrit et contribue à fournir des munitions aux critiques du site.
Créez des objets normaux et des objets à créneaux:
>>> class Foo(object): pass
>>> class Bar(object): __slots__ = ()
Instancier un million d'entre eux:
>>> foos = [Foo() for f in xrange(1000000)]
>>> bars = [Bar() for b in xrange(1000000)]
Inspecter 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, de types et classes d’unification dans Python 2.2
Si vous sous-classez un type intégré, de l'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 inquiéter 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 l'expression "__slots__ = []
" à votre classe.
Citant Jacob Hallen :
L'utilisation correcte de
__slots__
consiste à é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 le surcoût d’un dict pour chaque objet.] Bien que cette optimisation soit parfois utile, il serait totalement inutile que l’interprète Python soit suffisamment dynamique pour n’exige le dict que lorsqu’il y a eu des ajouts à l’objet.Malheureusement, les machines à sous ont un effet secondaire. Ils modifient le comportement des objets dotés d'emplacements de manière à ce qu'ils puissent être utilisés de manière abusive par les maniaques de contrôle et les marqueurs statiques. C’est mauvais, parce que les maniaques de contrôle devraient abuser des métaclasses et que les weenies de frappe statiques devraient abuser des décorateurs, puisqu’en Python, il ne devrait y avoir qu’une façon évidente de faire quelque chose.
Rendre CPython suffisamment intelligent pour gérer un gain de place sans
__slots__
est une entreprise majeure, ce qui explique probablement pourquoi il ne figure pas (encore) dans la liste des modifications apportées à P3k.
Vous voudriez utiliser __slots__
si vous voulez instancier beaucoup (des centaines, des milliers) d'objets de la même classe. __slots__
n'existe qu'en tant qu'outil d'optimisation de la mémoire.
Il est vivement déconseillé d'utiliser __slots__
pour limiter la création d'attributs. En règle générale, vous souhaitez l'éviter car cela casse la tâche, ainsi que d'autres fonctionnalités d'introspection de python.
Chaque objet python a un attribut __dict__
qui est un dictionnaire contenant tous les autres attributs. par exemple. lorsque vous tapez self.attr
python fait réellement self.__dict__['attr']
. Comme vous pouvez l'imaginer, utiliser un dictionnaire pour stocker l'attribut nécessite un espace et un temps supplémentaires pour y accéder.
Cependant, lorsque vous utilisez __slots__
, aucun objet créé pour cette classe n'aura d'attribut __dict__
. Au lieu de cela, tous les attributs sont accessibles 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 réduire la taille des objets et réduire le temps d'accès aux attributs. Un bon exemple est une classe de points contenant les attributs x et y. Si vous voulez avoir beaucoup de points, vous pouvez essayer d'utiliser __slots__
afin de conserver un peu de mémoire.
En plus des autres réponses, voici un exemple d'utilisation de __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 implémenter __slots__
, il ne faut qu'une ligne supplémentaire (et faire de votre classe une classe de style nouveau si ce n'est déjà fait). De cette façon, vous pouvez réduire de 5 fois l'encombrement de la mémoire de ces classes , au prix de l'écriture d'un code pickle personnalisé, si et quand cela devient nécessaire.
Les emplacements sont très utiles pour les appels de bibliothèque afin d'éliminer la "répartition de méthode nommée" lors d'appels de fonction. Ceci est mentionné dans le SWIG documentation . Pour les bibliothèques hautes performances qui souhaitent réduire le temps système pour les fonctions communément appelées utilisant des emplacements, c'est beaucoup plus rapide.
Maintenant, cela peut ne pas être directement lié à la question des PO. Cela concerne davantage la construction d'extensions que l'utilisation de la syntaxe slots sur un objet. Mais cela aide à compléter le tableau pour l'utilisation des créneaux horaires et une partie du raisonnement qui les sous-tend.
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 accès aux attributs habituels , l'instance fait office de dictionnaire et le nom de l'attribut sert de clé dans ce dictionnaire à la recherche valeur ajoutée.
instance (attribut) -> valeur
Dans __ slots__ access , le nom de l'attribut fait office de dictionnaire et l'instance sert de clé dans le dictionnaire. valeur.
attribut (instance) -> valeur
Dans modèle de poids mouche , le nom de l'attribut sert de dictionnaire et la valeur agit comme clé dans ce dictionnaire. l'instance.
attribut (valeur) -> instance
Une autre utilisation quelque peu obscure de __slots__
consiste à ajouter des attributs à un proxy d'objet à partir du package ProxyTypes, faisant autrefois partie du projet PEAK. Son ObjectWrapper
vous permet de créer un proxy pour un autre objet, mais d'intercepter toutes les interactions avec l'objet traité par proxy. Il n’est pas très utilisé (et pas de support 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 renvoie tous les accès à l’objet mandaté via le ioloop, en utilisant les objets thread-safe concurrent.Future
pour synchroniser et renvoyer les résultats.
Par défaut, tout attribut ayant accès à l'objet proxy vous donnera le résultat de l'objet traité par proxy. Si vous devez ajouter un attribut à l'objet proxy, vous pouvez utiliser __slots__
.
from peak.util.proxies import ObjectWrapper
class Original(object):
def __init__(self):
self.name = 'The Original'
class ProxyOriginal(ObjectWrapper):
__slots__ = ['proxy_name']
def __init__(self, subject, proxy_name):
# proxy_info attributed added directly to the
# Original instance, not the ProxyOriginal instance
self.proxy_info = 'You are proxied by {}'.format(proxy_name)
# proxy_name added to ProxyOriginal instance, since it is
# defined in __slots__
self.proxy_name = proxy_name
super(ProxyOriginal, self).__init__(subject)
if __== "__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
Un exemple très simple d'attribut __slot__
.
__slots__
Si je n'ai pas d'attribut __slot__
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 attribut dict
pour chaque objet (obj1 et obj2).
Supposons que si ma classe Test a des milliers de tels objets? La création d'un attribut supplémentaire dict
pour chaque objet entraînera beaucoup de temps système (mémoire, puissance de calcul, etc.) dans mon code.
__slots__
Maintenant, dans l'exemple suivant, ma classe Test contient l'attribut __slots__
. Maintenant, je ne peux pas ajouter de nouveaux attributs à mes objets (sauf l'attribut x
) et python ne crée plus d'attribut dict
. 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'
Vous avez - essentiellement - aucune utilisation de __slots__
.
Pour le moment où vous pensez avoir besoin de __slots__
, vous souhaitez réellement utiliser les modèles de conception Léger ou Poids léger. Dans certains cas, vous ne souhaitez plus utiliser uniquement des objets Python. Au lieu de cela, vous voulez un Python objet ressemblant à un objet 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
Le wrapper semblable à une classe n'a pas d'attributs - il fournit uniquement 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 aux seules fonctions opérant sur le tableau de données sous-jacent.
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 un meilleur résultat performance lors de l’instanciation de grandes quantités d’objets - intéressant, par exemple. lors de l'analyse de documents volumineux en objets ou à partir d'une base de données.
Voici une comparaison de la création d'arborescences d'objets avec un million d'entrées, en utilisant des emplacements et sans emplacements. À titre de référence, également les performances lors de l'utilisation de plans 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)
testcode, mode commenté:
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