Je veux pouvoir créer une classe (en Python) qui une fois initialisée avec __init__
, n'accepte pas les nouveaux attributs, mais accepte les modifications des attributs existants. Il y a plusieurs façons hack-ish que je peux voir pour faire cela, par exemple avoir un __setattr__
méthode telle que
def __setattr__(self, attribute, value):
if not attribute in self.__dict__:
print "Cannot set %s" % attribute
else:
self.__dict__[attribute] = value
puis en modifiant __dict__
directement à l'intérieur __init__
, mais je me demandais s'il y avait une "bonne" façon de procéder?
Je n'utiliserais pas __dict__
directement, mais vous pouvez ajouter une fonction pour "geler" explicitement une instance:
class FrozenClass(object):
__isfrozen = False
def __setattr__(self, key, value):
if self.__isfrozen and not hasattr(self, key):
raise TypeError( "%r is a frozen class" % self )
object.__setattr__(self, key, value)
def _freeze(self):
self.__isfrozen = True
class Test(FrozenClass):
def __init__(self):
self.x = 42#
self.y = 2**3
self._freeze() # no new attributes after this point.
a,b = Test(), Test()
a.x = 10
b.z = 10 # fails
Si quelqu'un est intéressé à le faire avec un décorateur, voici une solution de travail:
from functools import wraps
def froze_it(cls):
cls.__frozen = False
def frozensetattr(self, key, value):
if self.__frozen and not hasattr(self, key):
print("Class {} is frozen. Cannot set {} = {}"
.format(cls.__name__, key, value))
else:
object.__setattr__(self, key, value)
def init_decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
func(self, *args, **kwargs)
self.__frozen = True
return wrapper
cls.__setattr__ = frozensetattr
cls.__init__ = init_decorator(cls.__init__)
return cls
Assez simple à utiliser:
@froze_it
class Foo(object):
def __init__(self):
self.bar = 10
foo = Foo()
foo.bar = 42
foo.foobar = "no way"
Résultat:
>>> Class Foo is frozen. Cannot set foobar = no way
En fait, vous ne voulez pas __setattr__
, Vous voulez __slots__
. Ajoutez __slots__ = ('foo', 'bar', 'baz')
au corps de la classe et Python s'assurera qu'il n'y a que foo, bar et baz sur n'importe quelle instance. Mais lisez les mises en garde les listes de documentation!
La manière Pythonique est d'utiliser des machines à sous au lieu de jouer avec le __setter__
. Bien qu'il puisse résoudre le problème, il n'apporte aucune amélioration des performances. Les attributs des objets sont stockés dans un dictionnaire "__dict__
", c'est la raison pour laquelle vous pouvez ajouter dynamiquement des attributs aux objets des classes que nous avons créées jusqu'à présent. L'utilisation d'un dictionnaire pour le stockage des attributs est très pratique, mais cela peut signifier un gaspillage d'espace pour les objets, qui n'ont que une petite quantité de variables d'instance.
Slots sont une bonne façon de contourner ce problème de consommation d'espace. Au lieu d'avoir un dict dynamique qui permet d'ajouter dynamiquement des attributs aux objets, les slots fournissent une structure statique qui interdit les ajouts après la création d'une instance.
Lorsque nous concevons une classe, nous pouvons utiliser des emplacements pour empêcher la création dynamique d'attributs. Pour définir des emplacements, vous devez définir une liste avec le nom __slots__
. La liste doit contenir tous les attributs que vous souhaitez utiliser. Nous le démontrons dans la classe suivante, dans laquelle la liste des emplacements contient uniquement le nom d'un attribut "val".
class S(object):
__slots__ = ['val']
def __init__(self, v):
self.val = v
x = S(42)
print(x.val)
x.new = "not possible"
=> Il ne parvient pas à créer un attribut "nouveau":
42
Traceback (most recent call last):
File "slots_ex.py", line 12, in <module>
x.new = "not possible"
AttributeError: 'S' object has no attribute 'new'
NB:
Depuis Python 3.3 l'avantage d'optimiser la consommation d'espace n'est plus aussi impressionnant. Avec Python 3.3 Key-Sharing Les dictionnaires sont utilisés pour le stockage d'objets. Les attributs des instances sont capables de partager entre eux une partie de leur stockage interne, c'est-à-dire la partie qui stocke les clés et leurs hachages correspondants. Cela permet de réduire la consommation de mémoire des programmes, qui créent de nombreuses instances des types non intégrés. Mais il reste le chemin à parcourir pour éviter les attributs créés dynamiquement.
L'utilisation des machines à sous a également son propre coût. Cela rompra la sérialisation (par exemple, les cornichons). Il rompra également l'héritage multiple. Une classe ne peut pas hériter de plusieurs classes qui définissent des emplacements ou détiennent une disposition d'instance définie en code C (comme list, Tuple ou int).
La bonne façon est de remplacer __setattr__
. C'est pour ça qu'il est là.
J'aime beaucoup la solution qui utilise un décorateur, car elle est facile à utiliser pour de nombreuses classes à travers un projet, avec des ajouts minimum pour chaque classe. Mais cela ne fonctionne pas bien avec l'héritage. Voici donc ma version: elle ne remplace que la fonction __setattr__ - si l'attribut n'existe pas et que la fonction appelante n'est pas __init__, elle affiche un message d'erreur.
import inspect
def froze_it(cls):
def frozensetattr(self, key, value):
if not hasattr(self, key) and inspect.stack()[1][3] != "__init__":
print("Class {} is frozen. Cannot set {} = {}"
.format(cls.__name__, key, value))
else:
self.__dict__[key] = value
cls.__setattr__ = frozensetattr
return cls
@froze_it
class A:
def __init__(self):
self._a = 0
a = A()
a._a = 1
a._b = 2 # error
Et ça:
class A():
__allowed_attr=('_x', '_y')
def __init__(self,x=0,y=0):
self._x=x
self._y=y
def __setattr__(self,attribute,value):
if not attribute in self.__class__.__allowed_attr:
raise AttributeError
else:
super().__setattr__(attribute,value)
Voici l'approche que j'ai trouvée qui n'a pas besoin d'un attribut ou d'une méthode _frozen pour freeze () dans init.
Pendant init j'ajoute juste tous les attributs de classe à l'instance.
J'aime cela car il n'y a pas de _frozen, freeze () et _frozen n'apparaît pas non plus dans la sortie vars (instance).
class MetaModel(type):
def __setattr__(self, name, value):
raise AttributeError("Model classes do not accept arbitrary attributes")
class Model(object):
__metaclass__ = MetaModel
# init will take all CLASS attributes, and add them as SELF/INSTANCE attributes
def __init__(self):
for k, v in self.__class__.__dict__.iteritems():
if not k.startswith("_"):
self.__setattr__(k, v)
# setattr, won't allow any attributes to be set on the SELF/INSTANCE that don't already exist
def __setattr__(self, name, value):
if not hasattr(self, name):
raise AttributeError("Model instances do not accept arbitrary attributes")
else:
object.__setattr__(self, name, value)
# Example using
class Dog(Model):
name = ''
kind = 'canine'
d, e = Dog(), Dog()
print vars(d)
print vars(e)
e.junk = 'stuff' # fails
J'aime le "Frozen" de Jochen Ritzel. L'inconvénient est que la variable isfrozen apparaît alors lors de l'impression d'une classe .__ dict J'ai contourné ce problème de cette façon en créant une liste d'attributs autorisés (similaire à slots):
class Frozen(object):
__List = []
def __setattr__(self, key, value):
setIsOK = False
for item in self.__List:
if key == item:
setIsOK = True
if setIsOK == True:
object.__setattr__(self, key, value)
else:
raise TypeError( "%r has no attributes %r" % (self, key) )
class Test(Frozen):
_Frozen__List = ["attr1","attr2"]
def __init__(self):
self.attr1 = 1
self.attr2 = 1
Le FrozenClass
de Jochen Ritzel est cool, mais appeler _frozen()
lors de l'initialisation d'une classe à chaque fois n'est pas si cool (et vous devez prendre le risque de l'oublier). J'ai ajouté un __init_slots__
fonction:
class FrozenClass(object):
__isfrozen = False
def _freeze(self):
self.__isfrozen = True
def __init_slots__(self, slots):
for key in slots:
object.__setattr__(self, key, None)
self._freeze()
def __setattr__(self, key, value):
if self.__isfrozen and not hasattr(self, key):
raise TypeError( "%r is a frozen class" % self )
object.__setattr__(self, key, value)
class Test(FrozenClass):
def __init__(self):
self.__init_slots__(["x", "y"])
self.x = 42#
self.y = 2**3
a,b = Test(), Test()
a.x = 10
b.z = 10 # fails