Domanda

Voglio essere in grado di creare una classe (in Python) che una volta inizializzata con __init__, non accetta nuovi attributi, ma accetta modifiche degli attributi esistenti. Ci sono diversi modi di hack-ish che posso vedere per farlo, ad esempio avendo un __setattr__ metodo come

def __setattr__(self, attribute, value):
    if not attribute in self.__dict__:
        print "Cannot set %s" % attribute
    else:
        self.__dict__[attribute] = value

e poi editing __dict__ direttamente all'interno __init__, ma mi chiedevo se esiste un modo "corretto" per farlo?

È stato utile?

Soluzione

Non userei __dict__ direttamente, ma puoi aggiungere una funzione per "congelare" esplicitamente un'istanza:

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

Altri suggerimenti

Se qualcuno è interessato a farlo con un decoratore, ecco una soluzione funzionante:

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

Abbastanza semplice da usare:

@froze_it 
class Foo(object):
    def __init__(self):
        self.bar = 10

foo = Foo()
foo.bar = 42
foo.foobar = "no way"

Risultato:

>>> Class Foo is frozen. Cannot set foobar = no way

In realtà, non vuoi __setattr__, tu vuoi __slots__. Aggiungere __slots__ = ('foo', 'bar', 'baz') al corpo di classe e Python si assicurerà che ci sia solo foo, bar e baz in qualsiasi caso. Ma leggi gli avvertimenti degli elenchi di documentazione!

Gli slot sono la strada da percorrere:

Il modo pitonico è usare slot invece di giocare con il __setter__. Sebbene possa risolvere il problema, non fornisce alcun miglioramento delle prestazioni. Gli attributi degli oggetti sono archiviati in un dizionario "__dict__"Questo è il motivo per cui è possibile aggiungere dinamicamente gli attributi agli oggetti di classi che abbiamo creato finora. L'uso di un dizionario per la memoria degli attributi è molto conveniente, ma può significare uno spreco di spazio per gli oggetti, che hanno solo un piccolo quantità di variabili di istanza.

Slot sono un bel modo di aggirare questo problema di consumo di spazio. Invece di avere un DICT dinamico che consente di aggiungere attributi agli oggetti in modo dinamico, le slot forniscono una struttura statica che proibisce le aggiunte dopo la creazione di un'istanza.

Quando progettiamo una classe, possiamo usare slot per prevenire la creazione dinamica degli attributi. Per definire gli slot, devi definire un elenco con il nome __slots__. L'elenco deve contenere tutti gli attributi, che si desidera utilizzare. Lo dimostriamo nella classe seguente, in cui l'elenco delle slot contiene solo il nome per un attributo "Val".

class S(object):

    __slots__ = ['val']

    def __init__(self, v):
        self.val = v


x = S(42)
print(x.val)

x.new = "not possible"

=> Non riesce a creare un attributo "nuovo":

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. Poiché Python 3.3 il vantaggio di ottimizzare il consumo di spazio non è più così impressionante. Con Python 3.3 Sharing chiave I dizionari vengono utilizzati per la memorizzazione degli oggetti. Gli attributi delle istanze sono in grado di condividere parte della loro memoria interna tra loro, ovvero la parte che memorizza le chiavi e i loro hash corrispondenti. Questo aiuta a ridurre il consumo di memoria di programmi, che creano molte istanze di tipi non costruiti. Ma è ancora il modo di andare per evitare attributi creati in modo dinamico.

  2. L'uso delle slot viene anche con i suoi costi. Romperà la serializzazione (ad es. Pickle). Romperà anche l'eredità multipla. Una classe non può ereditare da più di una classe che definisce slot o cappello un layout di istanza definito nel codice C (come elenco, tuple o int).

Il modo corretto è sovrascrivere __setattr__. Questo è quello per cui è lì.

Mi piace molto la soluzione che utilizza un decoratore, perché è facile usarlo per molte classi attraverso un progetto, con aggiunte minime per ogni classe. Ma non funziona bene con l'eredità. Quindi ecco la mia versione: sovrascrive solo la funzione __setTR__: se l'attributo non esiste e la funzione chiamante non è __init__, stampa un messaggio di errore.

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

Ecco approccio che mi è venuto in mente che non ha bisogno di un attributo _frozen o metodo per congelare () in init.

In occasione dentro Aggiungo solo tutti gli attributi di classe all'istanza.

Mi piace questo perché non c'è _frozen, Freeze () e _frozen non viene visualizzato nell'output VARS (istanza).

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

Che dire di questo:

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)

Mi piace il "congelato" di Jochen Ritzel. Il scomodo è che il La variabile isfrozen appare quindi quando si stampano una classe .__ DICTHo esaminato questo problema in questo modo creando un elenco di attributi autorizzati (simile a slot):

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

Il FrozenClass Di Jochen Ritzel è bello, ma chiama _frozen() Quando si inizia una lezione ogni volta non è così bello (e devi correre il rischio di dimenticarla). Ho aggiunto un __init_slots__ funzione:

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

pystrict è Un decoratore installabile PYPI Ispirato da questa domanda di stackoverflow che può essere utilizzata con le classi per congelarle. C'è un esempio per il Readme che mostra perché un decoratore come questo è necessario anche se hai Mypy e Pylint in esecuzione sul tuo progetto:

pip install pystrict

Quindi usa solo il decoratore @strict:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top