¿Cómo hacer un objeto inmutable en Python?


Aunque nunca he necesitado esto, simplemente me llamó la atención que hacer un objeto inmutable en Python podría ser un poco complicado. No puedes simplemente anular __setattr__, porque entonces ni siquiera puedes establecer atributos en el __init__. La subclase de una tupla es un truco que funciona:

class Immutable(tuple):

    def __new__(cls, a, b):
        return tuple.__new__(cls, (a, b))

    @property
    def a(self):
        return self[0]

    @property
    def b(self):
        return self[1]

    def __str__(self):
        return "<Immutable {0}, {1}>".format(self.a, self.b)

    def __setattr__(self, *ignored):
        raise NotImplementedError

    def __delattr__(self, *ignored):
        raise NotImplementedError

Pero luego tienes acceso a las variables a y b a través de self[0] y self[1], lo cual es molesto.

¿Es esto posible en Python Puro? Si no, ¿cómo lo haría con una C extensión?

(Las respuestas que solo funcionan en Python 3 son aceptables).

Actualización:

Así que subclasificar tupla es la forma de hacerlo en Python puro, que funciona bien excepto por la posibilidad adicional de acceder a los datos por [0], [1] etc. Por lo tanto, para completar esta pregunta todo lo que falta es cómo hacerlo "correctamente" en C, lo que sospecho que sería bastante simple, simplemente no implementando ningún geititem o setattribute, etc. Pero en lugar de hacerlo yo mismo, ofrezco un recompensa por eso, porque soy perezoso. :)

Author: dbc, 2011-01-28

20 answers

Otra solución en la que acabo de pensar: La forma más sencilla de obtener el mismo comportamiento que su código original es

Immutable = collections.namedtuple("Immutable", ["a", "b"])

No resuelve el problema de que se puede acceder a los atributos a través de [0], etc., pero al menos es considerablemente más corto y proporciona la ventaja adicional de ser compatible con pickle y copy.

namedtuple crea un tipo similar al que describí en esta respuesta, es decir, derivado de tuple y usando __slots__. Está disponible en Python 2.6 o superior.

 94
Author: Sven Marnach,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:28

La forma más fácil de hacerlo es usando __slots__:

class A(object):
    __slots__ = []

Las instancias de A son inmutables ahora, ya que no puede establecer ningún atributo en ellas.

Si desea que las instancias de clase contengan datos, puede combinar esto con la derivación de tuple:

from operator import itemgetter
class Point(tuple):
    __slots__ = []
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))
    x = property(itemgetter(0))
    y = property(itemgetter(1))

p = Point(2, 3)
p.x
# 2
p.y
# 3

Editar : Si desea deshacerse de la indexación, puede anular __getitem__():

class Point(tuple):
    __slots__ = []
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))
    @property
    def x(self):
        return tuple.__getitem__(self, 0)
    @property
    def y(self):
        return tuple.__getitem__(self, 1)
    def __getitem__(self, item):
        raise TypeError

Tenga en cuenta que no puede usar operator.itemgetter para las propiedades en este caso, ya que esto dependería de Point.__getitem__() en lugar de tuple.__getitem__(). Además, esto no impedirá el uso de tuple.__getitem__(p, 0), pero difícilmente puedo imaginar cómo esto debería constituir un problema.

No creo que la forma "correcta" de crear un objeto inmutable sea escribir una extensión C. Python generalmente se basa en implementadores de bibliotecas y usuarios de bibliotecas siendo adultos que consienten , y en lugar de imponer realmente una interfaz, la interfaz debe estar claramente indicada en la documentación. Esta es la razón por la que no considero la posibilidad de eludir un se sobrescribe __setattr__() llamando a object.__setattr__() un problema. Si alguien hace esto, es bajo su propio riesgo.

 66
Author: Sven Marnach,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-02-01 04:44:34

..cómo hacerlo "correctamente" en C..

Puedes usar Cython para crear un tipo de extensión para Python:

cdef class Immutable:
    cdef readonly object a, b
    cdef object __weakref__ # enable weak referencing support

    def __init__(self, a, b):
        self.a, self.b = a, b

Funciona tanto en Python 2.x y 3.

Pruebas

# compile on-the-fly
import pyximport; pyximport.install() # $ pip install cython
from immutable import Immutable

o = Immutable(1, 2)
assert o.a == 1, str(o.a)
assert o.b == 2

try: o.a = 3
except AttributeError:
    pass
else:
    assert 0, 'attribute must be readonly'

try: o[1]
except TypeError:
    pass
else:
    assert 0, 'indexing must not be supported'

try: o.c = 1
except AttributeError:
    pass
else:
    assert 0, 'no new attributes are allowed'

o = Immutable('a', [])
assert o.a == 'a'
assert o.b == []

o.b.append(3) # attribute may contain mutable object
assert o.b == [3]

try: o.c
except AttributeError:
    pass
else:
    assert 0, 'no c attribute'

o = Immutable(b=3,a=1)
assert o.a == 1 and o.b == 3

try: del o.b
except AttributeError:
    pass
else:
    assert 0, "can't delete attribute"

d = dict(b=3, a=1)
o = Immutable(**d)
assert o.a == d['a'] and o.b == d['b']

o = Immutable(1,b=3)
assert o.a == 1 and o.b == 3

try: object.__setattr__(o, 'a', 1)
except AttributeError:
    pass
else:
    assert 0, 'attributes are readonly'

try: object.__setattr__(o, 'c', 1)
except AttributeError:
    pass
else:
    assert 0, 'no new attributes'

try: Immutable(1,c=3)
except TypeError:
    pass
else:
    assert 0, 'accept only a,b keywords'

for kwd in [dict(a=1), dict(b=2)]:
    try: Immutable(**kwd)
    except TypeError:
        pass
    else:
        assert 0, 'Immutable requires exactly 2 arguments'

Si no le importa el soporte de indexación, entonces collections.namedtuple sugerido por @Sven Marnach es preferible:

Immutable = collections.namedtuple("Immutable", "a b")
 48
Author: jfs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:03:02

Otra idea sería rechazar completamente __setattr__ y usar object.__setattr__ en el constructor:

class Point(object):
    def __init__(self, x, y):
        object.__setattr__(self, "x", x)
        object.__setattr__(self, "y", y)
    def __setattr__(self, *args):
        raise TypeError
    def __delattr__(self, *args):
        raise TypeError

Por supuesto que podría usar object.__setattr__(p, "x", 3) para modificar una instancia Point p, pero su implementación original sufre del mismo problema (pruebe tuple.__setattr__(i, "x", 42) en una instancia Immutable).

Puede aplicar el mismo truco en su implementación original: deshacerse de __getitem__() y usar tuple.__getitem__() en sus funciones de propiedad.

 34
Author: Sven Marnach,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-28 15:54:29

Puede crear un decorador @immutable que anule el __setattr__ y cambie el __slots__ a una lista vacía, luego decore el método __init__ con él.

Editar: Como señaló el OP, cambiar el atributo __slots__ solo impide la creación de nuevos atributos, no la modificación.

Edit2: Aquí hay una implementación:

Edit3: Usando __slots__ rompe este código, porque if detiene la creación del __dict__ del objeto. Estoy buscando un alternativa.

Edit4: Bueno, eso es todo. Es un pero hackish, pero funciona como un ejercicio: -)

class immutable(object):
    def __init__(self, immutable_params):
        self.immutable_params = immutable_params

    def __call__(self, new):
        params = self.immutable_params

        def __set_if_unset__(self, name, value):
            if name in self.__dict__:
                raise Exception("Attribute %s has already been set" % name)

            if not name in params:
                raise Exception("Cannot create atribute %s" % name)

            self.__dict__[name] = value;

        def __new__(cls, *args, **kws):
            cls.__setattr__ = __set_if_unset__

            return super(cls.__class__, cls).__new__(cls, *args, **kws)

        return __new__

class Point(object):
    @immutable(['x', 'y'])
    def __new__(): pass

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2) 
p.x = 3 # Exception: Attribute x has already been set
p.z = 4 # Exception: Cannot create atribute z
 17
Author: PaoloVictor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-28 15:13:50

No creo que sea completamente posible excepto usando una tupla o una namedtuple. Pase lo que pase, si anula __setattr__() el usuario siempre puede omitirlo llamando a object.__setattr__() directamente. Se garantiza que cualquier solución que dependa de __setattr__ no funcionará.

Lo siguiente es lo más cercano que puede obtener sin usar algún tipo de tupla:

class Immutable:
    __slots__ = ['a', 'b']
    def __init__(self, a, b):
        object.__setattr__(self, 'a', a)
        object.__setattr__(self, 'b', b)
    def __setattr__(self, *ignored):
        raise NotImplementedError
    __delattr__ = __setattr__

Pero se rompe si te esfuerzas lo suficiente:{[13]]}

>>> t = Immutable(1, 2)
>>> t.a
1
>>> object.__setattr__(t, 'a', 2)
>>> t.a
2

Pero el uso de Sven de {[8] } es genuinamente inmutable.

Update

Dado que la pregunta se ha actualizado para preguntar cómo hacerlo correctamente en C, aquí está mi respuesta sobre cómo hacerlo correctamente en Cython:

Primero immutable.pyx:

cdef class Immutable:
    cdef object _a, _b

    def __init__(self, a, b):
        self._a = a
        self._b = b

    property a:
        def __get__(self):
            return self._a

    property b:
        def __get__(self):
            return self._b

    def __repr__(self):
        return "<Immutable {0}, {1}>".format(self.a, self.b)

Y un setup.py para compilarlo (usando el comando setup.py build_ext --inplace:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [Extension("immutable", ["immutable.pyx"])]

setup(
  name = 'Immutable object',
  cmdclass = {'build_ext': build_ext},
  ext_modules = ext_modules
)

Entonces para probarlo:

>>> from immutable import Immutable
>>> p = Immutable(2, 3)
>>> p
<Immutable 2, 3>
>>> p.a = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'a' of 'immutable.Immutable' objects is not writable
>>> object.__setattr__(p, 'a', 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'a' of 'immutable.Immutable' objects is not writable
>>> p.a, p.b
(2, 3)
>>>      
 9
Author: Duncan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-21 03:29:25

Además de las excelentes otras respuestas, me gusta agregar un método para python 3.4 (o quizás 3.3). Esta respuesta se basa en varias respuestas previas a esta pregunta.

En python 3.4, puede usar propiedades sin setters para crear miembros de clase que no se pueden modificar. (En versiones anteriores era posible asignar propiedades sin un setter.)

class A:
    __slots__=['_A__a']
    def __init__(self, aValue):
      self.__a=aValue
    @property
    def a(self):
        return self.__a

Puedes usarlo así:

instance=A("constant")
print (instance.a)

Que imprimirá "constant"

Pero llamando instance.a=10 causará:

AttributeError: can't set attribute

Explicación: las propiedades sin setters son una característica muy reciente de python 3.4 (y creo que 3.3). Si intenta asignar a tal propiedad, se generará un error. Usando slots restrinjo los membervariables a __A_a (que es __a).

Problema: Asignar a _A__a todavía es posible (instance._A__a=2). Pero si asigna a una variable privada, es su propia culpa...

Esta respuesta entre otras, sin embargo, desalienta el uso de __slots__. Utilizar otras formas de evitar la creación de atributos podrían ser preferibles.

 4
Author: TheEspinosa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:11

He hecho clases inmutables sobreescribiendo __setattr__, y permitiendo el conjunto si el llamante es __init__:

import inspect
class Immutable(object):
    def __setattr__(self, name, value):
        if inspect.stack()[2][3] != "__init__":
            raise Exception("Can't mutate an Immutable: self.%s = %r" % (name, value))
        object.__setattr__(self, name, value)

Esto no es suficiente todavía, ya que permite a cualquier ___init__ cambiar el objeto, pero se tiene la idea.

 3
Author: Ned Batchelder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-28 12:51:29

Si está interesado en objetos con comportamiento, entonces namedtuple es casi su solución.

Como se describe en la parte inferior de la documentación de namedtuple , puede derivar su propia clase de namedtuple; y luego, puede agregar el comportamiento que desee.

Por ejemplo (código tomado directamente de la documentación ):

class Point(namedtuple('Point', 'x y')):
    __slots__ = ()
    @property
    def hypot(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    def __str__(self):
        return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

for p in Point(3, 4), Point(14, 5/7):
    print(p)

Esto resultará en:

Point: x= 3.000  y= 4.000  hypot= 5.000
Point: x=14.000  y= 0.714  hypot=14.018

Este enfoque funciona para Python 3 y Python 2.7 (probado en IronPython como bien).
El único inconveniente es que el árbol de herencia es un poco raro; pero esto no es algo con lo que normalmente juegas.

 3
Author: Roberto Liffredo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-29 18:47:33

Necesitaba esto hace un rato y decidí hacer un paquete Python para ello. La versión inicial está en PyPI ahora:

$ pip install immutable

Para usar:

>>> from immutable import ImmutableFactory
>>> MyImmutable = ImmitableFactory.create(prop1=1, prop2=2, prop3=3)
>>> MyImmutable.prop1
1

Documentos completos aquí: https://github.com/theengineear/immutable

Espero que ayude, envuelve un namedtuple como se ha discutido, pero hace que la instanciación sea mucho más simple.

 2
Author: theengineear,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-04 12:50:17

De esta manera no se detiene object.__setattr__ de trabajar, pero todavía lo he encontrado útil:

class A(object):

    def __new__(cls, children, *args, **kwargs):
        self = super(A, cls).__new__(cls)
        self._frozen = False  # allow mutation from here to end of  __init__
        # other stuff you need to do in __new__ goes here
        return self

    def __init__(self, *args, **kwargs):
        super(A, self).__init__()
        self._frozen = True  # prevent future mutation

    def __setattr__(self, name, value):
        # need to special case setting _frozen.
        if name != '_frozen' and self._frozen:
            raise TypeError('Instances are immutable.')
        else:
            super(A, self).__setattr__(name, value)

    def __delattr__(self, name):
        if self._frozen:
            raise TypeError('Instances are immutable.')
        else:
            super(A, self).__delattr__(name)

Es posible que necesite anular más cosas (como __setitem__) dependiendo del caso de uso.

 2
Author: dangirsh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-21 13:22:06

Aquí hay una solución elegante :

class Immutable(object):
    def __setattr__(self, key, value):
        if not hasattr(self, key):
            super().__setattr__(key, value)
        else:
            raise RuntimeError("Can't modify immutable object's attribute: {}".format(key))

Herede de esta clase, inicialice sus campos en el constructor, y estará todo listo.

 2
Author: Alexander Ryzhov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-29 04:32:45

A partir de Python 3.7, puede utilizar el @dataclass declarador en su clase y será inmutable como una estructura! Sin embargo, puede o no añadir un __hash__() método para su clase. Cita:

Hash() es usado por hash () incorporado, y cuando se agregan objetos a colecciones con hash como diccionarios y conjuntos. Tener un hash () implica que las instancias de la clase son inmutables. La mutabilidad es una propiedad complicada que depende de la intent, la existencia y el comportamiento de eq (), y los valores del eq y las banderas congeladas en el decorador dataclass ().

Por defecto, dataclass() no agregará implícitamente un método hash () a menos que sea seguro hacerlo. Tampoco añadirá ni cambiará un método hash hash() ya definido explícitamente. Establecer el atributo de clase hash = None tiene un significado específico para Python, como se describe en la documentación hash ().

Si hash () no está definido explícitamente, o si se establece en None, entonces dataclass() puede agregar un método implícito hash (). Aunque no se recomienda, puede forzar dataclass() para crear un método hash () con unsafe_hash=True. Este podría ser el caso si su clase es lógicamente inmutable, pero sin embargo puede ser mutada. Este es un caso de uso especializado y debe ser considerado cuidadosamente.

Aquí el ejemplo de los documentos enlazados:

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand
 2
Author: yugioh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-06 12:51:38

Las clases que heredan de la siguiente clase Immutable son inmutables, al igual que sus instancias, después de que su método __init__ termine de ejecutarse. Dado que es python puro, como otros han señalado, no hay nada que impida a alguien usar los métodos especiales mutantes de la base object y type, pero esto es suficiente para evitar que alguien mute una clase/instancia por accidente.

Funciona secuestrando el proceso de creación de clases con una metaclase.

"""Subclasses of class Immutable are immutable after their __init__ has run, in
the sense that all special methods with mutation semantics (in-place operators,
setattr, etc.) are forbidden.

"""  

# Enumerate the mutating special methods
mutation_methods = set()
# Arithmetic methods with in-place operations
iarithmetic = '''add sub mul div mod divmod pow neg pos abs bool invert lshift
                 rshift and xor or floordiv truediv matmul'''.split()
for op in iarithmetic:
    mutation_methods.add('__i%s__' % op)
# Operations on instance components (attributes, items, slices)
for verb in ['set', 'del']:
    for component in '''attr item slice'''.split():
        mutation_methods.add('__%s%s__' % (verb, component))
# Operations on properties
mutation_methods.update(['__set__', '__delete__'])


def checked_call(_self, name, method, *args, **kwargs):
    """Calls special method method(*args, **kw) on self if mutable."""
    self = args[0] if isinstance(_self, object) else _self
    if not getattr(self, '__mutable__', True):
        # self told us it's immutable, so raise an error
        cname= (self if isinstance(self, type) else self.__class__).__name__
        raise TypeError('%s is immutable, %s disallowed' % (cname, name))
    return method(*args, **kwargs)


def method_wrapper(_self, name):
    "Wrap a special method to check for mutability."
    method = getattr(_self, name)
    def wrapper(*args, **kwargs):
        return checked_call(_self, name, method, *args, **kwargs)
    wrapper.__name__ = name
    wrapper.__doc__ = method.__doc__
    return wrapper


def wrap_mutating_methods(_self):
    "Place the wrapper methods on mutative special methods of _self"
    for name in mutation_methods:
        if hasattr(_self, name):
            method = method_wrapper(_self, name)
            type.__setattr__(_self, name, method)


def set_mutability(self, ismutable):
    "Set __mutable__ by using the unprotected __setattr__"
    b = _MetaImmutable if isinstance(self, type) else Immutable
    super(b, self).__setattr__('__mutable__', ismutable)


class _MetaImmutable(type):

    '''The metaclass of Immutable. Wraps __init__ methods via __call__.'''

    def __init__(cls, *args, **kwargs):
        # Make class mutable for wrapping special methods
        set_mutability(cls, True)
        wrap_mutating_methods(cls)
        # Disable mutability
        set_mutability(cls, False)

    def __call__(cls, *args, **kwargs):
        '''Make an immutable instance of cls'''
        self = cls.__new__(cls)
        # Make the instance mutable for initialization
        set_mutability(self, True)
        # Execute cls's custom initialization on this instance
        self.__init__(*args, **kwargs)
        # Disable mutability
        set_mutability(self, False)
        return self

    # Given a class T(metaclass=_MetaImmutable), mutative special methods which
    # already exist on _MetaImmutable (a basic type) cannot be over-ridden
    # programmatically during _MetaImmutable's instantiation of T, because the
    # first place python looks for a method on an object is on the object's
    # __class__, and T.__class__ is _MetaImmutable. The two extant special
    # methods on a basic type are __setattr__ and __delattr__, so those have to
    # be explicitly overridden here.

    def __setattr__(cls, name, value):
        checked_call(cls, '__setattr__', type.__setattr__, cls, name, value)

    def __delattr__(cls, name, value):
        checked_call(cls, '__delattr__', type.__delattr__, cls, name, value)


class Immutable(object):

    """Inherit from this class to make an immutable object.

    __init__ methods of subclasses are executed by _MetaImmutable.__call__,
    which enables mutability for the duration.

    """

    __metaclass__ = _MetaImmutable


class T(int, Immutable):  # Checks it works with multiple inheritance, too.

    "Class for testing immutability semantics"

    def __init__(self, b):
        self.b = b

    @classmethod
    def class_mutation(cls):
        cls.a = 5

    def instance_mutation(self):
        self.c = 1

    def __iadd__(self, o):
        pass

    def not_so_special_mutation(self):
        self +=1

def immutabilityTest(f, name):
    "Call f, which should try to mutate class T or T instance."
    try:
        f()
    except TypeError, e:
        assert 'T is immutable, %s disallowed' % name in e.args
    else:
        raise RuntimeError('Immutability failed!')

immutabilityTest(T.class_mutation, '__setattr__')
immutabilityTest(T(6).instance_mutation, '__setattr__')
immutabilityTest(T(6).not_so_special_mutation, '__iadd__')
 1
Author: Alex Coventry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-27 13:07:26

El tercero attr el módulo proporciona esta funcionalidad.

$ pip install attrs
$ python
>>> @attr.s(frozen=True)
... class C(object):
...     x = attr.ib()
>>> i = C(1)
>>> i.x = 2
Traceback (most recent call last):
   ...
attr.exceptions.FrozenInstanceError: can't set attribute

attr implementa clases congeladas sobreescribiendo __setattr__ y tiene un impacto menor en el rendimiento en cada momento de instanciación, de acuerdo con la documentación.

Si tiene el hábito de usar clases como tipos de datos, attr puede ser especialmente útil ya que se encarga de la repetición por usted (pero no hace ninguna magia). En particular, escribe nueve métodos dunder (__X__) para usted (a menos que gire cualquiera de ellos off), incluyendo repr, init, hash y todas las funciones de comparación.

attr también proporciona un ayudante para __slots__.

 1
Author: cmc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-14 19:21:27

Un enfoque alternativo es crear una envoltura que haga que una instancia sea inmutable.

class Immutable(object):

    def __init__(self, wrapped):
        super(Immutable, self).__init__()
        object.__setattr__(self, '_wrapped', wrapped)

    def __getattribute__(self, item):
        return object.__getattribute__(self, '_wrapped').__getattribute__(item)

    def __setattr__(self, key, value):
        raise ImmutableError('Object {0} is immutable.'.format(self._wrapped))

    __delattr__ = __setattr__

    def __iter__(self):
        return object.__getattribute__(self, '_wrapped').__iter__()

    def next(self):
        return object.__getattribute__(self, '_wrapped').next()

    def __getitem__(self, item):
        return object.__getattribute__(self, '_wrapped').__getitem__(item)

immutable_instance = Immutable(my_instance)

Esto es útil en situaciones donde solo algunas instancias tienen que ser inmutables (como los argumentos predeterminados de las llamadas a funciones).

También se puede usar en fábricas inmutables como:

@classmethod
def immutable_factory(cls, *args, **kwargs):
    return Immutable(cls.__init__(*args, **kwargs))

También protege de object.__setattr__, pero falible a otros trucos debido a la naturaleza dinámica de Python.

 0
Author: Mark Horvath,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-01 14:50:31

Usé la misma idea que Alex: una meta-clase y un "marcador de inicio", pero en combinación con la sobreescritura__setattr__:

>>> from abc import ABCMeta
>>> _INIT_MARKER = '_@_in_init_@_'
>>> class _ImmutableMeta(ABCMeta):
... 
...     """Meta class to construct Immutable."""
... 
...     def __call__(cls, *args, **kwds):
...         obj = cls.__new__(cls, *args, **kwds)
...         object.__setattr__(obj, _INIT_MARKER, True)
...         cls.__init__(obj, *args, **kwds)
...         object.__delattr__(obj, _INIT_MARKER)
...         return obj
...
>>> def _setattr(self, name, value):
...     if hasattr(self, _INIT_MARKER):
...         object.__setattr__(self, name, value)
...     else:
...         raise AttributeError("Instance of '%s' is immutable."
...                              % self.__class__.__name__)
...
>>> def _delattr(self, name):
...     raise AttributeError("Instance of '%s' is immutable."
...                          % self.__class__.__name__)
...
>>> _im_dict = {
...     '__doc__': "Mix-in class for immutable objects.",
...     '__copy__': lambda self: self,   # self is immutable, so just return it
...     '__setattr__': _setattr,
...     '__delattr__': _delattr}
...
>>> Immutable = _ImmutableMeta('Immutable', (), _im_dict)

Nota: Estoy llamando a la meta-clase directamente para que funcione tanto para Python 2.x y 3.x.

>>> class T1(Immutable):
... 
...     def __init__(self, x=1, y=2):
...         self.x = x
...         self.y = y
...
>>> t1 = T1(y=8)
>>> t1.x, t1.y
(1, 8)
>>> t1.x = 7
AttributeError: Instance of 'T1' is immutable.

Funciona también con ranuras ...:

>>> class T2(Immutable):
... 
...     __slots__ = 's1', 's2'
... 
...     def __init__(self, s1, s2):
...         self.s1 = s1
...         self.s2 = s2
...
>>> t2 = T2('abc', 'xyz')
>>> t2.s1, t2.s2
('abc', 'xyz')
>>> t2.s1 += 'd'
AttributeError: Instance of 'T2' is immutable.

... y herencia múltiple:

>>> class T3(T1, T2):
... 
...     def __init__(self, x, y, s1, s2):
...         T1.__init__(self, x, y)
...         T2.__init__(self, s1, s2)
...
>>> t3 = T3(12, 4, 'a', 'b')
>>> t3.x, t3.y, t3.s1, t3.s2
(12, 4, 'a', 'b')
>>> t3.y -= 3
AttributeError: Instance of 'T3' is immutable.

Tenga en cuenta, sin embargo, que los atributos mutables permanecen para ser mutables:

>>> t3 = T3(12, [4, 7], 'a', 'b')
>>> t3.y.append(5)
>>> t3.y
[4, 7, 5]
 0
Author: Michael Amrhein,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-08 10:06:06

Una cosa que realmente no se incluye aquí es la inmutabilidad total... no solo el objeto padre, sino también todos los hijos. tuplas / frozensets pueden ser inmutables, por ejemplo, pero los objetos de los que forma parte pueden no serlo. Aquí hay una versión pequeña (incompleta) que hace un trabajo decente de imponer la inmutabilidad hasta el final:

# Initialize lists
a = [1,2,3]
b = [4,5,6]
c = [7,8,9]

l = [a,b]

# We can reassign in a list 
l[0] = c

# But not a tuple
t = (a,b)
#t[0] = c -> Throws exception
# But elements can be modified
t[0][1] = 4
t
([1, 4, 3], [4, 5, 6])
# Fix it back
t[0][1] = 2

li = ImmutableObject(l)
li
[[1, 2, 3], [4, 5, 6]]
# Can't assign
#li[0] = c will fail
# Can reference
li[0]
[1, 2, 3]
# But immutability conferred on returned object too
#li[0][1] = 4 will throw an exception

# Full solution should wrap all the comparison e.g. decorators.
# Also, you'd usually want to add a hash function, i didn't put
# an interface for that.

class ImmutableObject(object):
    def __init__(self, inobj):
        self._inited = False
        self._inobj = inobj
        self._inited = True

    def __repr__(self):
        return self._inobj.__repr__()

    def __str__(self):
        return self._inobj.__str__()

    def __getitem__(self, key):
        return ImmutableObject(self._inobj.__getitem__(key))

    def __iter__(self):
        return self._inobj.__iter__()

    def __setitem__(self, key, value):
        raise AttributeError, 'Object is read-only'

    def __getattr__(self, key):
        x = getattr(self._inobj, key)
        if callable(x):
              return x
        else:
              return ImmutableObject(x)

    def __hash__(self):
        return self._inobj.__hash__()

    def __eq__(self, second):
        return self._inobj.__eq__(second)

    def __setattr__(self, attr, value):
        if attr not in  ['_inobj', '_inited'] and self._inited == True:
            raise AttributeError, 'Object is read-only'
        object.__setattr__(self, attr, value)
 0
Author: Corley Brigman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-12 20:15:53

Simplemente puede anular setAttr en la instrucción final de init. Entonces puedes construir pero no cambiar. Obviamente todavía se puede anular por objeto usint. setAttr pero en la práctica la mayoría de los lenguajes tienen alguna forma de reflexión por lo que la inmutabilidad es siempre una abstracción con fugas. La inmutabilidad se trata más de evitar que los clientes violen accidentalmente el contrato de un objeto. Uso:

=============================

La solución original ofrecida era incorrecta, esto fue actualizado basado en los comentarios usando la solución de aquí

La solución original es incorrecta de una manera interesante, por lo que se incluye en la parte inferior.

===============================

class ImmutablePair(object):

    __initialised = False # a class level variable that should always stay false.
    def __init__(self, a, b):
        try :
            self.a = a
            self.b = b
        finally:
            self.__initialised = True #an instance level variable

    def __setattr__(self, key, value):
        if self.__initialised:
            self._raise_error()
        else :
            super(ImmutablePair, self).__setattr__(key, value)

    def _raise_error(self, *args, **kw):
        raise NotImplementedError("Attempted To Modify Immutable Object")

if __name__ == "__main__":

    immutable_object = ImmutablePair(1,2)

    print immutable_object.a
    print immutable_object.b

    try :
        immutable_object.a = 3
    except Exception as e:
        print e

    print immutable_object.a
    print immutable_object.b

Salida:

1
2
Attempted To Modify Immutable Object
1
2

======================================

Aplicación original:

Se señaló en los comentarios, correctamente, que esto no funciona de hecho, ya que impide la creación de más de un objeto como se está anulando la clase setattr método, lo que significa que un segundo no se puede crear como uno mismo.a = fallará en la segunda inicialización.

class ImmutablePair(object):

    def __init__(self, a, b):
        self.a = a
        self.b = b
        ImmutablePair.__setattr__ = self._raise_error

    def _raise_error(self, *args, **kw):
        raise NotImplementedError("Attempted To Modify Immutable Object")
 0
Author: phil_20686,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-29 10:00:12

La solución básica a continuación aborda el siguiente escenario:

  • __init__() se puede escribir accediendo a los atributos como de costumbre.
  • DESPUÉS de eso, el OBJETO se congela para atributos solo cambia:

La idea es anular el método __setattr__ y reemplazar su implementación cada vez que se cambie el estado de objeto congelado.

Así que necesitamos algún método (_freeze) que almacene esas dos implementaciones y cambie entre ellas cuando se solicite.

Esto mechanism puede implementarse dentro de la clase user o heredarse de una clase especial Freezer como se muestra a continuación:

class Freezer:
    def _freeze(self, do_freeze=True):
        def raise_sa(*args):            
            raise AttributeError("Attributes are frozen and can not be changed!")
        super().__setattr__('_active_setattr', (super().__setattr__, raise_sa)[do_freeze])

    def __setattr__(self, key, value):        
        return self._active_setattr(key, value)

class A(Freezer):    
    def __init__(self):
        self._freeze(False)
        self.x = 10
        self._freeze()
 0
Author: ipap,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-18 18:51:48