Pregunta

Quiero poder crear una clase (en Python) que una vez inicializada con __init__, no acepta nuevos atributos, pero acepta modificaciones de los atributos existentes. Hay varias formas de hack-ish que puedo ver para hacer esto, por ejemplo, tener un __setattr__ método como

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

y luego editar __dict__ directamente dentro __init__, ¿Pero me preguntaba si hay una forma "adecuada" de hacer esto?

¿Fue útil?

Solución

No usaría __dict__ directamente, pero puede agregar una función para "congelar" explícitamente una instancia:

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

Otros consejos

Si alguien está interesado en hacer eso con un decorador, aquí hay una solución de funcionamiento:

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

Bastante sencillo de usar:

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

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

Resultado:

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

En realidad, no quieres __setattr__, usted quiere __slots__. Agregar __slots__ = ('foo', 'bar', 'baz') Para el cuerpo de la clase, y Python se asegurará de que solo haya Foo, Bar y Baz en cualquier caso. ¡Pero lea las advertencias de las listas de documentación!

Las tragamonedas es el camino a seguir:

La forma pitónica es usar ranuras en lugar de jugar con el __setter__. Si bien puede resolver el problema, no da ninguna mejora del rendimiento. Los atributos de los objetos se almacenan en un diccionario "__dict__", Esta es la razón por la cual puede agregar dinámicamente atributos a los objetos de clases que hemos creado hasta ahora. Usar un diccionario para el almacenamiento de atributos es muy conveniente, pero puede significar un desperdicio de espacio para objetos, que tienen solo un pequeño cantidad de variables de instancia.

Ranura son una buena manera de solucionar este problema de consumo de espacio. En lugar de tener un dict dinámico que permita agregar atributos a los objetos dinámicamente, las ranuras proporcionan una estructura estática que prohíbe las adiciones después de la creación de una instancia.

Cuando diseñamos una clase, podemos usar ranuras para evitar la creación dinámica de atributos. Para definir ranuras, debes definir una lista con el nombre __slots__. La lista debe contener todos los atributos que desea usar. Demostramos esto en la siguiente clase, en la que la lista de ranuras contiene solo el nombre de un atributo "Val".

class S(object):

    __slots__ = ['val']

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


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

x.new = "not possible"

=> No puede crear un atributo "nuevo":

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'

NÓTESE BIEN:

  1. Dado que Python 3.3, la ventaja que optimiza el consumo de espacio ya no es tan impresionante. Con Python 3.3 Compartir llave Los diccionarios se utilizan para el almacenamiento de objetos. Los atributos de las instancias son capaces de compartir parte de su almacenamiento interno entre sí, es decir, la parte que almacena las teclas y sus hashes correspondientes. Esto ayuda a reducir el consumo de memoria de los programas, que crean muchas instancias de tipos de no construcción. Pero sigue siendo el camino a seguir para evitar atributos creados dinámicamente.

  2. El uso de ranuras viene también con su propio costo. Romperá la serialización (por ejemplo, Pickle). También romperá la herencia múltiple. Una clase no puede heredar de más de una clase que define las ranuras o un diseño de instancia definido en el código C (como List, Tuple o Int).

La forma correcta es anular __setattr__. Para eso es para.

Me gusta mucho la solución que usa un decorador, porque es fácil usarla para muchas clases en un proyecto, con adiciones mínimas para cada clase. Pero no funciona bien con la herencia. Así que aquí está mi versión: solo anula la función __setattr__: si el atributo no existe y la función de llamadas no es __init__, imprime un mensaje de error.

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

Aquí hay un enfoque que se me ocurrió que no necesita un atributo o método de Frozen para congelar () en init.

Durante en eso Solo agrego todos los atributos de clase a la instancia.

Me gusta esto porque no hay _frozen, congele (), y _frozen tampoco aparece en la salida vars (instancia).

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

¿Qué pasa con esto?

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)

Me gusta el "congelado" de Jochen Ritzel. El inconveniente es que el La variable isFrozen aparece cuando se imprime una clase .__ DICTFui a este problema de esta manera creando una lista de atributos autorizados (similar a ranura):

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

los FrozenClass por Jochen Ritzel es genial, pero llamando _frozen() Cuando inicializar una clase cada vez no es tan genial (y debe correr el riesgo de olvidarla). Agregué un __init_slots__ función:

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 es un decorador instalable de Pypi Inspirado en esta pregunta de StackOverflow que se puede usar con clases para congelarlas. Hay un ejemplo para el ReadMe que muestra por qué se necesita un decorador como este, incluso si tiene Mypy y Pylint en su proyecto:

pip install pystrict

Entonces usa el decorador @strict:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top