Evitar la creación de nuevos atributos fuera de __init__


82

Quiero poder crear una clase (en Python) que una vez inicializada con __init__, no acepta nuevos atributos, pero acepta modificaciones de atributos existentes. Hay varias formas pirateadas 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.


1
katrielalex aporta buenos puntos. No tiene nada de malo. Podrías evitar el uso, __setattr__pero probablemente sería un truco.
aaronasterling

No veo por qué esto es hacky. Es la mejor solución que se me ocurrió y mucho más sucinta que algunas de las otras propuestas.
Chris B

Respuestas:


81

No lo 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

¡Muy genial! Creo que tomaré ese fragmento de código y comenzaré a usarlo. (Hmm, me pregunto si se podría hacer como decorador, o si eso no sería una buena idea ...)
weronika

5
Comentario tardío: estuve usando esta receta con éxito durante algún tiempo, hasta que cambié un atributo a una propiedad, donde el captador estaba generando un NotImplementedError. Me tomó mucho tiempo descubrir que esto se debía al hecho de que actualmente hasattrllama getattr, descarta el resultado y devuelve False en caso de errores, consulte este blog . Encontré una solución reemplazando not hasattr(self, key)por key not in dir(self). Esto podría ser más lento, pero resolvió el problema por mí.
Bas Swinckels

31

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

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

+1 para la versión decorador. Eso es lo que usaría para un proyecto más grande, en un script más grande esto es exagerado (tal vez si lo tuvieran en la biblioteca estándar ...). Por ahora solo hay "advertencias de estilo IDE".
Tomasz Gandor

2
¿Cómo funciona esta solución con el patrimonio? por ejemplo, si tengo una clase secundaria de Foo, ¿esta clase secundaria es por defecto una clase congelada?
mrgiesel

¿Hay un paquete de pypi para este decorador?
winni2k

¿Cómo se puede mejorar el decorador para que funcione con clases heredadas?
Ivan Nechipayko

30

Slots es el camino a seguir:

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

Las tragamonedas son una buena forma de solucionar este problema de consumo de espacio. En lugar de tener un dictado dinámico que permite agregar atributos a los objetos de forma dinámica, los espacios 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 slots 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 desee utilizar. 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 crea 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. Desde Python 3.3, la ventaja de optimizar el consumo de espacio ya no es tan impresionante. Con Python 3.3 , los diccionarios de uso compartido de claves 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 claves y sus correspondientes hashes. Esto ayuda a reducir el consumo de memoria de los programas, que crean muchas instancias de tipos no integrados. Pero todavía es el camino a seguir para evitar atributos creados dinámicamente.
  1. El uso de las tragamonedas también tiene 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 ranuras o tiene un diseño de instancia definido en código C (como lista, tupla o int).

20

En realidad, no quieres __setattr__, quieres __slots__. Agregue __slots__ = ('foo', 'bar', 'baz')al cuerpo de la clase y Python se asegurará de que solo haya foo, bar y baz en cualquier instancia. ¡Pero lea las advertencias en las listas de documentación!


12
Usar __slots__funciona, pero romperá la serialización (por ejemplo, pickle), entre otras cosas ... Suele ser una mala idea usar ranuras para controlar la creación de atributos, en lugar de reducir la sobrecarga de memoria, en mi opinión, de todos modos ...
Joe Kington

Lo sé, y dudo en usarlo yo mismo, pero hacer un trabajo adicional para no permitir nuevos atributos también suele ser una mala idea;)

2
El uso __slots__también rompe la herencia múltiple. Una clase no puede heredar de más de una clase que define ranuras o tiene un diseño de instancia definido en código C (como list, tupleo int).
Feuermurmel

Si __slots__rompe sus pepinillos, está utilizando un protocolo antiguo de pepinillos. Pase protocol=-1a los métodos pickle para el protocolo más reciente disponible, que es 2 en Python 2 ( introducido en 2003 ). Los protocolos predeterminados y más recientes de Python 3 (3 y 4 respectivamente) manejan __slots__.
Nick Matteo

bueno, la mayoría de las veces termino
Erik Aronesty

7

La forma correcta es anular __setattr__. Para eso está ahí.


Entonces, ¿cuál es la forma correcta de establecer variables __init__? ¿Es colocarlos __dict__directamente?
astrofrog

1
Me anular __setattr__en __init__, por self.__setattr__ = <new-function-that-you-just-defined>.
Katriel

6
@katrielalex: eso no funcionará para las clases de nuevo estilo, ya que los __xxx__métodos solo se buscan en la clase, no en la instancia.
Ethan Furman

6

Me gusta mucho la solución que usa un decorador, porque es fácil de usar 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 llamada 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

4

¿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)

2

Aquí está el enfoque que se me ocurrió que no necesita un atributo o método _frozen para congelar () en init.

Durante el inicio, solo agrego todos los atributos de clase a la instancia.

Me gusta esto porque no hay _frozen, freeze (), y _frozen tampoco aparece en la salida de 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

Esto no parece funcionar si uno de los campos es una lista. Digamos names=[]. Luego d.names.append['Fido']insertará 'Fido'en ambos d.namesy e.names. No sé lo suficiente sobre Python para entender por qué.
Reinier Torenbeek

2

pystrictes un decorador instalable de pypi inspirado en esta pregunta de stackoverflow que se puede usar con clases para congelarlas. Hay un ejemplo en README que muestra por qué se necesita un decorador como este incluso si tiene mypy y pylint ejecutándose en su proyecto:

pip install pystrict

Entonces solo usa el decorador @strict:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1

1

Me gusta el "Frozen" de Jochen Ritzel. El inconveniente es que la variable isfrozen aparece al imprimir una clase .__ dict. Resolví este problema de esta manera creando una lista de atributos autorizados (similar a las ranuras ):

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

El FrozenClassde Jochen Ritzel es genial, pero llamar _frozen()cuando se inicializa una clase cada vez no es tan genial (y debes correr el riesgo de olvidarlo). Agregué una __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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.