web-dev-qa-db-fra.com

Empêcher la création de nouveaux attributs en dehors de __init__

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?

67
astrofrog

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
66
Jochen Ritzel

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

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!

19
user395760

Les machines à sous sont le chemin à parcourir:

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:

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

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

17
DhiaTN

La bonne façon est de remplacer __setattr__. C'est pour ça qu'il est là.

6
Katriel

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
4
Eran Friedman

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)
3
Clementerf

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

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
1
Arthur Bauville

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