¿Cómo hacer un objeto inmutable en Python?

181

Aunque nunca he necesitado esto, me llamó la atención que hacer un objeto inmutable en Python podría ser un poco complicado. No puede anular simplemente __setattr__, porque entonces ni siquiera puede establecer atributos en el __init__. Subclasificar 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 entonces usted tiene acceso a la ay blas variables a través self[0]y self[1], lo que es molesto.

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

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

Actualizar:

Entonces, subclasificar tuplas es la forma de hacerlo en Pure Python, que funciona bien, excepto por la posibilidad adicional de acceder a los datos [0], [1]etc. Entonces, para completar esta pregunta, todo lo que falta es cómo hacerlo "correctamente" en C, que Sospecho que sería bastante simple, simplemente no implementando ninguno geititemo setattributeetc. Pero en lugar de hacerlo yo mismo, ofrezco una recompensa por eso, porque soy flojo. :)

Lennart Regebro
fuente
2
¿Su código no facilita el acceso a los atributos a través de .ay .b? Para eso parecen existir las propiedades después de todo.
Sven Marnach
1
@Sven Marnach: Sí, pero [0] y [1] todavía funcionan, ¿y por qué lo harían? No los quiero a ellos. :) ¿Quizás la idea de un objeto inmutable con atributos no tiene sentido? :-)
Lennart Regebro
2
Solo otra nota: NotImplementedsolo se entiende como un valor de retorno para las comparaciones ricas. De __setatt__()todos modos, un valor de retorno para es bastante inútil, ya que generalmente no lo verá en absoluto. Código como immutable.x = 42silenciosamente no hará nada. Deberías plantear un TypeErroren su lugar.
Sven Marnach
1
@Sven Marnach: OK, me sorprendió, porque pensé que podrías plantear NotImplemented en esta situación, pero eso da un error extraño. Así que lo devolví, y pareció funcionar. TypeError tenía sentido cuando vi que lo usaste.
Lennart Regebro
1
@Lennart: Puedes subir NotImplementedError, pero TypeErrores lo que aumenta una tupla si intentas modificarlo.
Sven Marnach

Respuestas:

115

Otra solución más en la que acabo de pensar: la forma más simple 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 pickley copy.

namedtuplecrea un tipo similar al que describí en esta respuesta , es decir, derivado tupley utilizado __slots__. Está disponible en Python 2.6 o superior.

Sven Marnach
fuente
77
La ventaja de esta variante en comparación con analógico escrito a mano (incluso en Python 2.5 (usando verboseparámetro para namedtupleel código se genera fácilmente)) es la única interfaz / implementación de un namedtuplees preferible a docenas muy ligeramente diferentes interfaces escritas a mano / implementaciones que hacer casi lo mismo.
jfs
2
OK, obtienes la "mejor respuesta", porque es la forma más fácil de hacerlo. Sebastian obtiene la recompensa por dar una implementación corta de Cython. ¡Salud!
Lennart Regebro
1
Otra característica de los objetos inmutables es que cuando los pasa como parámetro a través de una función, se copian por valor, en lugar de que se haga otra referencia. ¿ namedtupleSería copiado por valor cuando se pasa a través de funciones?
hlin117
44
@ hlin117: cada parámetro se pasa como referencia a un objeto en Python, independientemente de si es mutable o inmutable. Para los objetos inmutables, sería particularmente inútil hacer una copia, ya que de todos modos no puede cambiar el objeto, también puede pasar una referencia al objeto original.
Sven Marnach
¿Se puede usar namedtuple internamente dentro de la clase en lugar de instanciar el objeto externamente? Soy muy nuevo en Python, pero la ventaja de su otra respuesta es que puedo hacer que una clase oculte los detalles y también tenga el poder de cosas como parámetros opcionales. Si solo miro esta respuesta, parece que necesito tener todo lo que usa mi instancia de clase llamada tuplas. Gracias por las dos respuestas.
Asaf
78

La forma más fácil de hacer esto es usar __slots__:

class A(object):
    __slots__ = []

Las instancias de Ason 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 derivar 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.itemgetterlas propiedades en este caso, ya que esto dependería en Point.__getitem__()lugar de tuple.__getitem__(). Además, esto no impedirá el uso de tuple.__getitem__(p, 0), pero apenas 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 que son adultos que dan consentimiento , y en lugar de imponer realmente una interfaz, la interfaz debe estar claramente indicada en la documentación. Es por eso que no considero la posibilidad de eludir una anulación __setattr__()llamando a object.__setattr__()un problema. Si alguien hace esto, es bajo su propio riesgo.

Sven Marnach
fuente
1
¿No sería una mejor idea usar un tupleaquí __slots__ = (), en lugar de __slots__ = []? (Solo aclarando)
user225312
1
@sukhbir: Creo que esto no importa en absoluto. ¿Por qué preferirías una tupla?
Sven Marnach
1
@Sven: Estoy de acuerdo en que no importaría (excepto la parte de velocidad, que podemos ignorar), pero lo pensé de esta manera: __slots__no se va a cambiar, ¿verdad? Su propósito es identificar por una vez qué atributos se pueden establecer. Entonces, ¿no tupleparece una elección muy natural en tal caso?
user225312
55
Pero con un vacío __slots__no puedo establecer ningún atributo. Y si tengo, __slots__ = ('a', 'b')entonces los atributos ayb siguen siendo mutables.
Lennart Regebro
Pero su solución es mejor que anular, __setattr__por lo que es una mejora sobre la mía. +1 :)
Lennart Regebro
50

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

Puede 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.xy 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 es preferible que collections.namedtuplesugiera @Sven Marnach

Immutable = collections.namedtuple("Immutable", "a b")
jfs
fuente
@Lennart: las instancias de namedtuple(o más precisamente del tipo devuelto por la función namedtuple()) son inmutables. Seguro.
Sven Marnach
@Lennart Regebro: namedtuplepasa todas las pruebas (excepto el soporte de indexación). ¿Qué requisito me perdí?
jfs
Sí, tiene razón, hice un tipo de tupla con nombre, lo instanciaron y luego hice la prueba en el tipo en lugar de la instancia. Je :-)
Lennart Regebro
¿Puedo preguntar por qué se necesita una referencia débil aquí?
McSinyx
1
@McSinyx: de lo contrario, los objetos no se pueden usar en las colecciones de weakref. ¿Qué es exactamente __weakref__en Python?
jfs
40

Otra idea sería no permitir __setattr__y usar por completo 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, podría usar object.__setattr__(p, "x", 3)para modificar una Pointinstancia p, pero su implementación original tiene el mismo problema (pruebe tuple.__setattr__(i, "x", 42)con unImmutable instancia).

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

Sven Marnach
fuente
11
No me importaría que alguien modificara deliberadamente el objeto usando la superclase ' __setattr__, porque el punto no es ser infalible. El punto es dejar en claro que no debe modificarse y evitar modificaciones por error.
zvone
18

Puede crear un @immutabledecorador que anule __setattr__ o cambie el __slots__a una lista vacía, luego decore el __init__método con él.

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

Edit2: Aquí hay una implementación:

Edit3: el uso __slots__rompe este código, porque si detiene la creación del objeto __dict__. Estoy buscando una alternativa.

Edit4: Bueno, eso es todo. Es un poco duro, 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
PaoloVictor
fuente
1
Hacer una decoradora (¿clase?) O una metaclase con la solución es una buena idea, pero la pregunta es cuál es la solución. :)
Lennart Regebro
3
object.__setattr__()se rompe stackoverflow.com/questions/4828080/…
jfs
En efecto. Simplemente seguí como ejercicio en decoradores.
PaoloVictor
13

Usando una clase de datos congelada

Para Python 3.7+ puede usar una clase de datos con unfrozen=True opción , que es una forma muy pitónica y fácil de hacer para hacer lo que quieras.

Se vería algo así:

from dataclasses import dataclass

@dataclass(frozen=True)
class Immutable:
    a: Any
    b: Any

Como se requieren sugerencias de tipo para los campos de clases de datos, he utilizado Any del typingmódulo .

Razones para NO usar un Namedtuple

Antes de Python 3.7 era frecuente ver a las tuplas nombradas como objetos inmutables. Puede ser complicado de muchas maneras, una de ellas es que el __eq__método entre las tuplas nombradas no considera las clases de los objetos. Por ejemplo:

from collections import namedtuple

ImmutableTuple = namedtuple("ImmutableTuple", ["a", "b"])
ImmutableTuple2 = namedtuple("ImmutableTuple2", ["a", "c"])

obj1 = ImmutableTuple(a=1, b=2)
obj2 = ImmutableTuple2(a=1, c=2)

obj1 == obj2  # will be True

Como puede ver, incluso si los tipos de obj1y obj2son diferentes, incluso si los nombres de sus campos son diferentes, obj1 == obj2todavía da True. Esto se debe a que el __eq__método utilizado es el de la tupla, que compara solo los valores de los campos en función de sus posiciones. Esa puede ser una gran fuente de errores, especialmente si está subclasificando estas clases.

Jundiaius
fuente
10

No creo que sea completamente posible, excepto mediante el uso de una tupla o una tupla con nombre. Pase lo que pase, si anula __setattr__()el usuario, siempre puede omitirlo llamando object.__setattr__()directamente. Cualquier solución que dependa de__setattr__ está garantizada para no funcionar.

Lo siguiente es sobre lo más cercano que puede llegar 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:

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

pero el uso de Sven namedtuplees genuinamente inmutable.

Actualizar

Como 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 a setup.pypara 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
)

Luego 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)
>>>      
Duncan
fuente
Gracias por el código de Cython, Cython es increíble. La implementación de JF Sebastians con el readonly es más ordenada y llegó primero, sin embargo, por lo que obtiene la recompensa.
Lennart Regebro
5

Hice clases inmutables anulando __setattr__y permitiendo el conjunto si la persona que llama 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 aún no es suficiente, ya que permite que cualquiera ___init__pueda cambiar el objeto, pero se entiende la idea.

Ned Batchelder
fuente
object.__setattr__()se rompe stackoverflow.com/questions/4828080/…
jfs
3
Usar la inspección de la pila para garantizar que la persona que llama __init__no sea muy satisfactoria.
gb.
5

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 puedan 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 llamar instance.a=10causará:

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 dicha propiedad, se generará un error. Usando ranuras restrinjo las variables miembro a __A_a(que es __a).

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

Esta respuesta, entre otras, sin embargo, desalienta el uso de __slots__. Usar otras formas de prevenir la creación de atributos puede ser preferible.

Bernhard
fuente
propertytambién está disponible en Python 2 (mira el código en la pregunta en sí). No crea un objeto inmutable, pruebe las pruebas de mi respuesta , por ejemplo, instance.b = 1crea un nuevo batributo.
jfs
Correcto, la pregunta es realmente cómo evitar hacerlo, A().b = "foo"es decir, no permitir establecer nuevos atributos.
Lennart Regebro
Las propiedades sin un instalador generan un error en Python 3.4 si intenta asignar a esa propiedad. En versiones anteriores, el setter se generaba implícitamente.
Bernhard
@Lennart: Mi solución es una respuesta a un subconjunto de casos de uso para objetos inmutables y una adición a las respuestas anteriores. Una razón por la que podría querer un objeto inmutable es para poder hacerlo hashaable, en cuyo caso mi solución podría funcionar. Pero tienes razón, este no es un objeto inmutable.
Bernhard
@ jf-sebastian: Cambié mi respuesta para usar ranuras para evitar la creación de atributos. Lo nuevo en mi respuesta en comparación con otras respuestas es que uso las propiedades de python3.4 para evitar cambiar los atributos existentes. Si bien lo mismo se logra en las respuestas anteriores, mi código es más corto debido al cambio en el comportamiento de las propiedades.
Bernhard
5

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á listo.

Alexander Ryzhov
fuente
1
pero con esta lógica es posible asignar nuevos atributos al objeto
javed
3

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

Como se describe al final 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 tanto para Python 3 como para Python 2.7 (también probado en IronPython).
El único inconveniente es que el árbol de herencia es un poco extraño; pero esto no es algo con lo que sueles jugar.

robar
fuente
1
Python 3.6+ lo admite directamente, usandoclass Point(typing.NamedTuple):
Elazar
3

Las clases que heredan de la siguiente Immutableclase son inmutables, como son sus instancias, una vez que su __init__método termina de ejecutarse. Como se trata de Python puro, como han señalado otros, no hay nada que impida que alguien use los métodos especiales de mutación de la base objectytype , pero esto es suficiente para detener a nadie de la mutación de 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__')
Alex Coventry
fuente
2

Lo necesitaba hace un momento y decidí hacer un paquete de Python para él. La versión inicial está en PyPI ahora:

$ pip install immutable

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 una tupla con nombre como se ha discutido, pero hace que la instanciación sea mucho más simple.

theengineear
fuente
2

De esta manera no deja object.__setattr__de funcionar, pero aún lo encuentro ú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 deba anular más cosas (como __setitem__) según el caso de uso.

dangirsh
fuente
Se me ocurrió algo similar antes de ver esto, pero lo usé getattrpara poder proporcionar un valor predeterminado para frozen. Eso simplificó un poco las cosas. stackoverflow.com/a/22545808/5987
Mark Ransom
Me gusta más este enfoque, pero no necesita la __new__anulación. En el interior __setattr__solo reemplaza el condicional conif name != '_frozen' and getattr(self, "_frozen", False)
Pete Cacioppi
Además, no hay necesidad de congelar la clase después de la construcción. Puede congelarlo en cualquier momento si proporciona una freeze()función. El objeto se "congelará una vez". Finalmente, preocuparse object.__setattr__es una tontería, porque "aquí todos somos adultos".
Pete Cacioppi
2

¡A partir de Python 3.7, puede usar el @dataclassdecorador en su clase y será inmutable como una estructura! Sin embargo, puede o no agregar un __hash__()método a su clase. Citar:

hash () es usado por hash () incorporado, y cuando los objetos se agregan a colecciones 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 intención del programador, la existencia y el comportamiento de eq (), y los valores de eq y banderas congeladas en el decorador dataclass ().

Por defecto, dataclass () no agregará implícitamente un método hash () a menos que sea seguro hacerlo. Tampoco agregará o cambiará un método hash () explícitamente definido . 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 está establecido en Ninguno, 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 puede ser mutada. Este es un caso de uso especializado y debe considerarse cuidadosamente.

Aquí el ejemplo de los documentos vinculados anteriormente:

@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
Comunidad
fuente
1
necesita usar frozen, es decir @dataclass(frozen=True), pero básicamente bloquea el uso __setattr__y me __delattr__gusta en la mayoría de las otras respuestas aquí. Simplemente lo hace de manera compatible con las otras opciones de clases de datos.
CS
2

Puede anular setattr y seguir usando init para establecer la variable. Usarías super class setattr . Aquí está el código.

clase inmutable:
    __slots__ = ('a', 'b')
    def __init __ (self, a, b):
        super () .__ setattr __ ('a', a)
        super () .__ setattr __ ('b', b)

    def __str __ (self):
        return "" .format (self.a, self.b)

    def __setattr __ (self, * ignorado):
        raise NotImplementedError

    def __delattr __ (self, * ignorado):
        raise NotImplementedError
Shameer Ali
fuente
O simplemente en passlugar deraise NotImplementedError
jonathan.scholbach
No es una buena idea hacer "pasar" en __setattr__ y __delattr__ en este caso. La razón simple es que si alguien asigna un valor a un campo / propiedad, entonces, naturalmente, esperan que se cambie el campo. Si desea seguir el camino de la "menor sorpresa" (como debería), debe generar un error. Pero no estoy seguro si NotImplementedError es el correcto para subir. Plantearía algo como "El campo / propiedad es inmutable". error ... Creo que se debe lanzar una excepción personalizada.
darlove
1

El attrmódulo de terceros proporciona esta funcionalidad. .

Editar: python 3.7 ha adoptado esta idea en stdlib con @dataclass.

$ 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 anulando __setattr__ y tiene un impacto menor en el rendimiento en cada tiempo de instanciación, de acuerdo con la documentación.

Si tiene la costumbre de usar clases como tipos de datos, attrpuede ser especialmente útil ya que se ocupa de la placa repetitiva por usted (pero no hace ninguna magia). En particular, escribe nueve métodos dunder (__X__) para usted (a menos que desactive alguno de ellos), incluidos repr, init, hash y todas las funciones de comparación.

attrTambién proporciona un ayudante para__slots__ .

cmc
fuente
1

Entonces, estoy escribiendo respectivo de python 3:

I) con la ayuda del decorador de clase de datos y establecer frozen = True. Podemos crear objetos inmutables en Python.

para esto es necesario importar la clase de datos de las clases de datos lib y necesita configurar frozen = True

ex.

desde clases de datos importar clase de datos

@dataclass(frozen=True)
class Location:
    name: str
    longitude: float = 0.0
    latitude: float = 0.0

o / p:

l = Ubicación ("Delhi", 112.345, 234.788) l.name 'Delhi' l.longitud 112.345 l.latitud 234.788 l.name = clases de datos "Kolkata". FrozenInstanceError: no se puede asignar al campo 'nombre'

Fuente: https://realpython.com/python-data-classes/

RaghuGolla
fuente
0

Un enfoque alternativo es crear un contenedor 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 object.__setattr__, pero puede fallar a otros trucos debido a la naturaleza dinámica de Python.

Mark Horvath
fuente
0

Usé la misma idea que Alex: una metaclase y un "marcador de inicio", pero en combinación con sobreescribir __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: Llamo a la metaclase directamente para que funcione tanto para Python 2.xy 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 máquinas tragamonedas ...:

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

Sin embargo, tenga en cuenta que los atributos mutables siguen siendo mutables:

>>> t3 = T3(12, [4, 7], 'a', 'b')
>>> t3.y.append(5)
>>> t3.y
[4, 7, 5]
Michael Amrhein
fuente
0

Una cosa que realmente no se incluye aquí es la inmutabilidad total ... no solo el objeto padre, sino también todos los hijos. Las tuplas / congelados 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)
Corley Brigman
fuente
0

Puede anular setAttr en la declaración final de init. Entonces puedes construir pero no cambiar. Obviamente, aún puede anular por el objeto usint. setAttr pero en la práctica la mayoría de los idiomas tienen alguna forma de reflexión, por lo que la inmutabilidad siempre es una abstracción permeable. La inmutabilidad se trata más de evitar que los clientes violen accidentalmente el contrato de un objeto. Yo suelo:

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

La solución original ofrecida era incorrecta, esta se actualizó en función de los comentarios utilizando 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

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

Implementación original:

Se señaló en los comentarios, correctamente, que esto de hecho no funciona, ya que impide la creación de más de un objeto, ya que está anulando el método de clase setattr, lo que significa que no se puede crear un segundo como self.a = will 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")
phil_20686
fuente
1
Eso no funcionará: está anulando el método en la clase , por lo que obtendrá NotImplementedError tan pronto como intente crear una segunda instancia.
slinkp
1
Si desea seguir este enfoque, tenga en cuenta que es difícil anular métodos especiales en tiempo de ejecución: consulte stackoverflow.com/a/16426447/137635 para obtener un par de soluciones a esto.
slinkp
0

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 que el OBJETO esté congelado solo para cambios de atributos :

La idea es anular el __setattr__método y reemplazar su implementación cada vez que se cambia el estado del objeto congelado.

Por lo tanto, necesitamos algún método ( _freeze) que almacene esas dos implementaciones y cambie entre ellas cuando se solicite.

Este mecanismo puede implementarse dentro de la clase de usuario o heredarse de una Freezerclase especial 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()
ipap
fuente
0

Como un dict

Tengo una biblioteca de código abierto donde hago las cosas de manera funcional , por lo que es útil mover los datos en un objeto inmutable. Sin embargo, no quiero tener que transformar mi objeto de datos para que el cliente interactúe con ellos. Entonces, se me ocurrió esto: te da un dict como objeto inmutable + algunos métodos auxiliares.

Gracias a Sven Marnach en su respuesta por la implementación básica de restringir la actualización y eliminación de propiedades.

import json 
# ^^ optional - If you don't care if it prints like a dict
# then rip this and __str__ and __repr__ out

class Immutable(object):

    def __init__(self, **kwargs):
        """Sets all values once given
        whatever is passed in kwargs
        """
        for k,v in kwargs.items():
            object.__setattr__(self, k, v)

    def __setattr__(self, *args):
        """Disables setting attributes via
        item.prop = val or item['prop'] = val
        """
        raise TypeError('Immutable objects cannot have properties set after init')

    def __delattr__(self, *args):
        """Disables deleting properties"""
        raise TypeError('Immutable objects cannot have properties deleted')

    def __getitem__(self, item):
        """Allows for dict like access of properties
        val = item['prop']
        """
        return self.__dict__[item]

    def __repr__(self):
        """Print to repl in a dict like fashion"""
        return self.pprint()

    def __str__(self):
        """Convert to a str in a dict like fashion"""
        return self.pprint()

    def __eq__(self, other):
        """Supports equality operator
        immutable({'a': 2}) == immutable({'a': 2})"""
        if other is None:
            return False
        return self.dict() == other.dict()

    def keys(self):
        """Paired with __getitem__ supports **unpacking
        new = { **item, **other }
        """
        return self.__dict__.keys()

    def get(self, *args, **kwargs):
        """Allows for dict like property access
        item.get('prop')
        """
        return self.__dict__.get(*args, **kwargs)

    def pprint(self):
        """Helper method used for printing that
        formats in a dict like way
        """
        return json.dumps(self,
            default=lambda o: o.__dict__,
            sort_keys=True,
            indent=4)

    def dict(self):
        """Helper method for getting the raw dict value
        of the immutable object"""
        return self.__dict__

Métodos de ayuda

def update(obj, **kwargs):
    """Returns a new instance of the given object with
    all key/val in kwargs set on it
    """
    return immutable({
        **obj,
        **kwargs
    })

def immutable(obj):
    return Immutable(**obj)

Ejemplos

obj = immutable({
    'alpha': 1,
    'beta': 2,
    'dalet': 4
})

obj.alpha # 1
obj['alpha'] # 1
obj.get('beta') # 2

del obj['alpha'] # TypeError
obj.alpha = 2 # TypeError

new_obj = update(obj, alpha=10)

new_obj is not obj # True
new_obj.get('alpha') == 10 # True
rayepps
fuente