Como criar um objeto imutável em Python?

181

Embora eu nunca precisei disso, me ocorreu que criar um objeto imutável em Python poderia ser um pouco complicado. Você não pode simplesmente substituir __setattr__, porque não pode nem mesmo definir atributos no arquivo __init__. Subclassificar uma tupla é um truque 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

Mas então você tem acesso ao ae bvariáveis através de self[0]e self[1], o que é irritante.

Isso é possível no Pure Python? Caso contrário, como eu faria isso com uma extensão C?

(Respostas que funcionam apenas no Python 3 são aceitáveis).

Atualizar:

Então subclasse tupla é a maneira de fazê-lo em pura Python, que funciona bem, exceto para a possibilidade adicional de acessar os dados por [0], [1]etc. Então, para completar esta questão tudo o que está faltando é howto fazê-lo "corretamente" em C, que Eu suspeito que seria bem simples, simplesmente não implementando nenhum geititemou setattributeetc. Mas, em vez de fazer isso sozinho, ofereço uma recompensa por isso, porque sou preguiçoso. :)

Lennart Regebro
fonte
2
Seu código não facilita o acesso aos atributos via .ae .b? Afinal, é para isso que as propriedades existem.
Sven Marnach
1
@Ven Marnach: Sim, mas [0] e [1] ainda funcionam, e por que eles? Eu não os quero. :) Talvez a idéia de um objeto imutável com atributos seja um absurdo? :-)
Lennart Regebro
2
Apenas outra observação: NotImplementedserve apenas como um valor de retorno para comparações completas. De __setatt__()qualquer forma, um valor de retorno para é inútil, pois você geralmente não o verá. Código como immutable.x = 42silenciosamente não fará nada. Você deve aumentar um em TypeErrorvez disso.
Sven Marnach
1
@Sven Marnach: OK, fiquei surpreso, porque pensei que você poderia aumentar o NotImplemented nessa situação, mas isso dá um erro estranho. Então, devolvi-o e parecia funcionar. TypeError fazia sentido quando vi que você o usava.
Lennart Regebro
1
@Lennart: Você pode aumentar NotImplementedError, mas TypeErroré o que uma tupla aumenta se você tentar modificá-la.
Sven Marnach

Respostas:

115

Outra solução que pensei: a maneira mais simples de obter o mesmo comportamento que o código original é

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

Ele não resolve o problema de que os atributos podem ser acessados ​​via [0]etc., mas pelo menos é consideravelmente mais curto e oferece a vantagem adicional de ser compatível com picklee copy.

namedtuplecria um tipo semelhante ao que eu descrevi nesta resposta , ou seja, derivado tuplee usado __slots__. Está disponível no Python 2.6 ou superior.

Sven Marnach
fonte
7
A vantagem dessa variante em comparação ao analógico escrito à mão (mesmo no Python 2.5 (o uso do verboseparâmetro para namedtupleo código é facilmente gerado)) é que a única interface / implementação de um namedtupleé preferível a dezenas de interfaces / implementações manuscritas ligeiramente diferentes que faça quase a mesma coisa.
jfs
2
OK, você obtém a "melhor resposta", porque é a maneira mais fácil de fazer isso. Sebastian recebe a recompensa por fornecer uma implementação curta do Cython. Felicidades!
Lennart Regebro
1
Outra característica dos objetos imutáveis ​​é que, quando você os passa como parâmetro por meio de uma função, eles são copiados por valor, em vez de outra referência ser feita. Seria namedtuplecopiado por valor quando passado por funções?
hlin117
4
@ hlin117: Cada parâmetro é passado como referência a um objeto em Python, independentemente de ser mutável ou imutável. Para objetos imutáveis, seria particularmente inútil fazer uma cópia - já que você não pode alterar o objeto de qualquer maneira, também pode passar uma referência ao objeto original.
Sven Marnach
Você pode usar o nomeado duplo internamente dentro da classe em vez de instanciar o objeto externamente? Eu sou muito novo em python, mas a vantagem de sua outra resposta é que posso fazer com que uma classe oculte os detalhes e também tenha o poder de coisas como parâmetros opcionais. Se eu olhar apenas para esta resposta, parece que preciso ter tudo o que usa minha instância de classe nomeada tuplas. Obrigado pelas duas respostas.
Asaf
78

A maneira mais fácil de fazer isso é usando __slots__:

class A(object):
    __slots__ = []

Instâncias de Aagora são imutáveis, já que você não pode definir nenhum atributo nelas.

Se você deseja que as instâncias da classe contenham dados, você pode combinar isso com a derivação 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 : se você quiser se livrar da indexação, pode substituir __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

Observe que você não pode usar operator.itemgetteras propriedades neste caso, pois isso dependeria em Point.__getitem__()vez de tuple.__getitem__(). Além disso, isso não impedirá o uso tuple.__getitem__(p, 0), mas mal posso imaginar como isso deve constituir um problema.

Eu não acho que a maneira "certa" de criar um objeto imutável seja escrever uma extensão em C. O Python geralmente depende de implementadores de bibliotecas e usuários de bibliotecas aceitando adultos e, em vez de realmente impor uma interface, a interface deve ser claramente declarada na documentação. É por isso que não considero a possibilidade de contornar uma substituição substituindo-a __setattr__()por object.__setattr__()um problema. Se alguém faz isso, é por sua conta e risco.

Sven Marnach
fonte
1
Não seria uma idéia melhor usar um tupleaqui __slots__ = (), em vez de __slots__ = []? (Just esclarecendo)
user225312
1
@sukhbir: Eu acho que isso não importa. Por que você prefere uma tupla?
Sven Marnach
1
@ Sven: Concordo que isso não importaria (exceto a parte da velocidade, que podemos ignorar), mas pensei da seguinte maneira: __slots__não será alterado, certo? Seu objetivo é identificar por uma vez quais atributos podem ser definidos. Então, não tupleparece uma escolha muito natural nesse caso?
precisa saber é o seguinte
5
Mas com um vazio, __slots__não consigo definir nenhum atributo. E se eu tiver __slots__ = ('a', 'b'), os atributos aeb ainda são mutáveis.
Lennart Regebro
Mas sua solução é melhor do que substituir, __setattr__portanto é uma melhoria em relação à minha. 1 :)
Lennart Regebro
50

..como fazê-lo "corretamente" em C ..

Você pode usar o Cython para criar um tipo de extensão 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

Ele funciona tanto em Python 2.xe em 3.

Testes

# 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'

Se você não se importa com o suporte à indexação, é preferívelcollections.namedtuple sugerir @Sven Marnach :

Immutable = collections.namedtuple("Immutable", "a b")
jfs
fonte
@Lennart: Instâncias de namedtuple(ou mais precisamente do tipo retornado pela função namedtuple()) são imutáveis. Definitivamente.
Sven Marnach
@Lennart Regebro: namedtuplepassa em todos os testes (exceto no suporte à indexação). Que requisito eu perdi?
JFS
Sim, você está certo, eu criei um tipo de nome duplo, instanciei e fiz o teste no tipo em vez da instância. Heh. :-)
Lennart Regebro
posso perguntar por que alguém precisaria de referências fracas aqui?
McSinyx
1
@ McSinyx: caso contrário, os objetos não poderão ser usados ​​nas coleções de fracaref. O que exatamente está __weakref__em Python?
jfs 22/03
40

Outra idéia seria desabilitar completamente __setattr__e usar object.__setattr__no construtor:

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

Claro que você pode usar object.__setattr__(p, "x", 3)para modificar uma Pointinstância p, mas sua implementação original sofre do mesmo problema (tente tuple.__setattr__(i, "x", 42)uma Immutableinstância).

Você pode aplicar o mesmo truque em sua implementação original: livrar-se __getitem__()e usar tuple.__getitem__()em suas funções de propriedade.

Sven Marnach
fonte
11
Eu não me importaria se alguém modificasse deliberadamente o objeto usando a superclasse ' __setattr__, porque o objetivo não é ser infalível. O objetivo é deixar claro que não deve ser modificado e evitar modificações por engano.
Zvone
18

Você pode criar um @immutabledecorador que substitua __setattr__ e mude __slots__para uma lista vazia e decore o__init__ método com ele.

Editar: como o OP observou, alterar o __slots__atributo apenas impede a criação de novos atributos , não a modificação.

Edit2: Aqui está uma implementação:

Edit3: Using __slots__quebra este código, porque se interrompe a criação do objeto__dict__ . Estou procurando uma alternativa.

Edit4: Bem, é isso. É um truque, mas funciona como um exercício :-)

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
fonte
1
Criar um decorador (de classe?) Ou uma metaclasse da solução é realmente uma boa ideia, mas a questão é qual é a solução. :)
Lennart Regebro
3
object.__setattr__()O que é isso? stackoverflow.com/questions/4828080/…
jfs 31/01
De fato. Eu apenas continuei como um exercício de decoradores.
PaoloVictor 31/01
13

Usando uma Dataclass congelada

Para o Python 3.7+, você pode usar uma Data Class com uma frozen=Trueopção , que é uma maneira muito pitonica e de manutenção de fazer o que você deseja.

Seria algo assim:

from dataclasses import dataclass

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

Como dica de tipo é necessária para os campos das classes de dados, usei Qualquer um do typingmódulo .

Razões para NÃO usar um Namedtuple

Antes do Python 3.7, era frequente ver os nomeados serem usados ​​como objetos imutáveis. Pode ser complicado de várias maneiras, uma delas é que o __eq__método entre os nomeados não considera as classes dos objetos. Por exemplo:

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 você vê, mesmo que os tipos de obj1e obj2sejam diferentes, mesmo que os nomes de seus campos sejam diferentes, obj1 == obj2ainda dá True. Isso __eq__ocorre porque o método usado é o da tupla, que compara apenas os valores dos campos, dadas suas posições. Isso pode ser uma fonte enorme de erros, especialmente se você estiver subclassificando essas classes.

Jundiaius
fonte
10

Não acho que seja inteiramente possível, exceto usando uma tupla ou um nomeado. Não importa o que, se você substituir __setattr__()o usuário sempre poderá ignorá-lo ligando object.__setattr__()diretamente. Qualquer solução que dependa __setattr__é garantida para não funcionar.

A seguir, é o mais próximo que você pode chegar sem usar algum 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__

mas quebra se você se esforçar o suficiente:

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

mas o uso de Sven namedtupleé genuinamente imutável.

Atualizar

Como a pergunta foi atualizada para perguntar como fazê-lo corretamente em C, eis a minha resposta sobre como fazê-lo corretamente no Cython:

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

e a setup.pypara compilá-lo (usando o 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
)

Então, experimente:

>>> 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
fonte
Obrigado pelo código Cython, o Cython é incrível. A implementação de JF Sebastians com o readonly é mais limpa e chegou primeiro, então ele recebe a recompensa.
Lennart Regebro
5

Eu criei classes imutáveis ​​substituindo __setattr__e permitindo o conjunto se o chamador for __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)

Isso ainda não é suficiente, pois permite que qualquer pessoa ___init__altere o objeto, mas você entendeu.

Ned Batchelder
fonte
object.__setattr__()O que é isso? stackoverflow.com/questions/4828080/…
jfs 31/01
3
Usar a inspeção de pilha para garantir que o chamador __init__não seja muito satisfatório.
gb.
5

Além das excelentes outras respostas, eu gostaria de adicionar um método para o python 3.4 (ou talvez 3.3). Esta resposta baseia-se em várias respostas anteriores para esta pergunta.

No python 3.4, você pode usar propriedades sem setters para criar membros da classe que não podem ser modificados. (Nas versões anteriores, era possível atribuir propriedades sem um setter.)

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

Você pode usá-lo assim:

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

que imprimirá "constant"

Mas chamar instance.a=10causará:

AttributeError: can't set attribute

Explicação: propriedades sem setters são um recurso muito recente do python 3.4 (e acho que 3.3). Se você tentar atribuir a essa propriedade, um erro será gerado. Usando slots, restrinja as variáveis ​​de membro para __A_a(o que é __a).

Problema: _A__aainda é possível atribuir a ( instance._A__a=2). Mas se você atribuir a uma variável privada, a culpa é sua ...

Essa resposta, entre outros, desencoraja o uso de __slots__. Usar outras maneiras de impedir a criação de atributos pode ser preferível.

Bernhard
fonte
propertytambém está disponível no Python 2 (veja o código na própria pergunta). Ele não cria um objeto imutável, tente os testes da minha resposta , por exemplo, instance.b = 1cria um novo batributo.
JFS
Certo, a questão é realmente como evitar fazer, A().b = "foo"ou seja, não permitir a configuração de novos atributos.
Lennart Regebro 02/07
As propriedades sem um setter geram um erro no python 3.4 se você tentar atribuir a essa propriedade. Nas versões anteriores, o setter era gerado implicitamente.
Bernhard
@Lennart: Minha solução é uma resposta a um subconjunto de casos de uso de objetos imutáveis ​​e uma adição a respostas anteriores. Uma razão pela qual eu posso querer um objeto imutável é poder torná-lo lavável, caso em que minha solução pode funcionar. Mas você está correto, este não é um objeto imutável.
Bernhard
@ jf-sebastian: Alterei minha resposta para usar slots para impedir a criação de atributos. O que há de novo na minha resposta em comparação com outras respostas é que eu uso as propriedades do python3.4 para evitar a alteração de atributos existentes. Embora o mesmo seja alcançado nas respostas anteriores, meu código é mais curto por causa da alteração no comportamento das propriedades.
Bernhard
5

Aqui está uma solução 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))

Herdar desta classe, inicialize seus campos no construtor e está tudo pronto.

Alexander Ryzhov
fonte
1
mas com essa lógica é possível atribuir novos atributos ao objeto
javed 12/03/19
3

Se você estiver interessado em objetos com comportamento, o nomeado duplo é quase a sua solução.

Conforme descrito na parte inferior da documentação do nomeado , você pode derivar sua própria classe do nomeado; e, em seguida, você pode adicionar o comportamento desejado.

Por exemplo (código extraído diretamente da documentação ):

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)

Isso resultará em:

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

Essa abordagem funciona para o Python 3 e o Python 2.7 (testados no IronPython também).
A única desvantagem é que a árvore de herança é um pouco estranha; mas isso não é algo com que você costuma brincar.

roubar
fonte
1
O Python 3.6+ suporta isso diretamente, usando #class Point(typing.NamedTuple):
Elazar
3

As classes que herdam da Immutableclasse a seguir são imutáveis, assim como suas instâncias, após o término da __init__execução do método. Como é python puro, como outros já apontaram, não há nada que impeça alguém de usar os métodos especiais mutantes da base objectetype , mas isso é suficiente para impedir que alguém mude uma classe / instância por acidente.

Ele funciona seqüestrando o processo de criação de classe com uma metaclasse.

"""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
fonte
2

Eu precisava disso há pouco tempo e decidi fazer um pacote Python para ele. A versão inicial está no PyPI agora:

$ pip install immutable

Usar:

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

Documentos completos aqui: https://github.com/theengineear/immutable

Espero que ajude, envolva um duplo nomeado conforme discutido, mas simplifique muito a instanciação.

theengineear
fonte
2

Dessa forma, não para object.__setattr__de funcionar, mas ainda assim achei ú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)

pode ser necessário substituir mais itens (como __setitem__), dependendo do caso de uso.

dangirsh
fonte
Eu vim com algo semelhante antes de ver isso, mas usei getattrpara fornecer um valor padrão para frozen. Isso simplificou um pouco as coisas. stackoverflow.com/a/22545808/5987
Mark Ransom
Eu gosto dessa abordagem da melhor maneira, mas você não precisa da __new__substituição. Dentro __setattr__basta substituir o condicional porif name != '_frozen' and getattr(self, "_frozen", False)
Pete Cacioppi 7/15
Além disso, não há necessidade de congelar a classe na construção. Você pode congelá-lo a qualquer momento se fornecer uma freeze()função. O objeto será "congelado uma vez". Finalmente, preocupar-se object.__setattr__é bobagem, porque "somos todos adultos aqui".
Pete Cacioppi
2

A partir do Python 3.7, você pode usar o @dataclassdecorador da sua classe e ele será imutável como uma estrutura! No entanto, ele pode ou não adicionar um __hash__()método à sua classe. Citar:

hash () é usado por hash interno () e quando objetos são adicionados a coleções de hash, como dicionários e conjuntos. Ter um hash () implica que instâncias da classe são imutáveis. A mutabilidade é uma propriedade complicada que depende da intenção do programador, da existência e do comportamento de eq () e dos valores dos sinalizadores eq e congelados no decorador dataclass ().

Por padrão, dataclass () não adiciona implicitamente um método hash (), a menos que seja seguro fazê-lo. Nem adicionará ou alterará um método hash () definido explicitamente existente . Definir o atributo de classe hash = None tem um significado específico para Python, conforme descrito na documentação de hash ().

Se hash () não estiver definido explicitamente, ou se estiver definido como None, dataclass () poderá adicionar um método implícito hash (). Embora não seja recomendado, você pode forçar dataclass () a criar um método hash () com unsafe_hash = True. Esse pode ser o caso se sua classe for logicamente imutável, mas ainda assim puder ser alterada. Este é um caso de uso especializado e deve ser considerado com cuidado.

Aqui está o exemplo dos documentos vinculados acima:

@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
Comunidade
fonte
1
você precisa usar frozen, ou seja @dataclass(frozen=True), mas basicamente bloqueia o uso de __setattr__e __delattr__como na maioria das outras respostas aqui. Ele apenas faz isso de uma maneira que seja compatível com as outras opções de classes de dados.
CS
2

Você pode substituir o setattr e ainda usar init para definir a variável. Você usaria a super classe setattr . aqui está o código

classe Imutável:
    __slots__ = ('a', 'b')
    def __init __ (auto, a, b):
        super () .__ setattr __ ('a', a)
        super () .__ setattr __ ('b', b)

    def __str __ (próprio):
        retornar "". formato (self.a, self.b)

    def __setattr __ (próprio, * ignorado):
        aumentar NotImplementedError

    def __delattr __ (próprio, * ignorado):
        aumentar NotImplementedError
Shameer Ali
fonte
Ou apenas em passvez deraise NotImplementedError
jonathan.scholbach
Não é uma boa ideia executar "pass" em __setattr__ e __delattr__ nesse caso. O motivo simples é que, se alguém atribui um valor a um campo / propriedade, então naturalmente espera que o campo seja alterado. Se você quiser seguir o caminho da "menor surpresa" (como deveria), precisará gerar um erro. Mas não tenho certeza se NotImplementedError é o caminho certo para aumentar. Eu levantaria algo como "O campo / propriedade é imutável". erro ... Eu acho que uma exceção personalizada deve ser lançada.
darlove
1

O attrmódulo de terceiros fornece essa funcionalidade .

Edit: python 3.7 adotou essa idéia no stdlib com @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

attrimplementa classes congeladas substituindo __setattr__e tem um impacto menor no desempenho a cada momento da instanciação, de acordo com a documentação.

Se você tem o hábito de usar classes como tipos de dados, attrpode ser especialmente útil, pois cuida do clichê para você (mas não faz mágica). Em particular, ele escreve nove métodos dunder (__X__) para você (a menos que você desative qualquer um deles), incluindo repr, init, hash e todas as funções de comparação.

attrtambém fornece um auxiliar para__slots__ .

cmc
fonte
1

Então, eu estou escrevendo o respectivo python 3:

I) com a ajuda do decorador da classe de dados e defina frozen = True. podemos criar objetos imutáveis ​​em python.

para isso, é necessário importar a classe de dados das classes de dados lib e precisa configurar frozen = True

ex.

de classes de dados importar classe de dados

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

o / p:

l = Localização ("Delhi", 112.345, 234.788) l.name 'Delhi' l.longitude 112.345 l.latitude 234.788 l.name = dataklasses de "Calcutá ".FrozenInstanceError: não é possível atribuir ao campo 'name'

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

RaghuGolla
fonte
0

Uma abordagem alternativa é criar um wrapper que torne uma instância imutável.

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)

Isso é útil em situações em que apenas algumas instâncias precisam ser imutáveis ​​(como argumentos padrão de chamadas de função).

Também pode ser usado em fábricas imutáveis ​​como:

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

Também protege object.__setattr__, mas é passível de outros truques devido à natureza dinâmica do Python.

Mark Horvath
fonte
0

Eu usei a mesma idéia que Alex: uma meta-classe e um "marcador de inicialização", mas em combinação com sobrescrita __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: Estou chamando a meta-classe diretamente para fazê-la funcionar no Python 2.xe 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.

Também funciona com slots ...:

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

... e herança múltipla:

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

Observe, no entanto, que atributos mutáveis ​​permanecem mutáveis:

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

Uma coisa que realmente não está incluída aqui é a imutabilidade total ... não apenas o objeto pai, mas também todas as crianças. tuplas / frozensets podem ser imutáveis, por exemplo, mas os objetos dos quais fazem parte podem não ser. Aqui está uma versão pequena (incompleta) que faz um trabalho decente ao impor a imutabilidade até o fim:

# 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
fonte
0

Você pode simplesmente substituir setAttr na declaração final do init. Então você pode construir, mas não mudar. Obviamente você ainda pode substituir por usint objeto. setAttr mas na prática a maioria das linguagens possui alguma forma de reflexão, de modo que a imutabilidade é sempre uma abstração com vazamento. Imutabilidade é mais para impedir que os clientes violem acidentalmente o contrato de um objeto. Eu uso:

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

A solução original oferecida estava incorreta, foi atualizada com base nos comentários usando a solução daqui

A solução original está errada de uma maneira interessante, por isso está incluída na 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

Resultado :

1
2
Attempted To Modify Immutable Object
1
2

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

Implementação original:

Foi indicado nos comentários, corretamente, que isso realmente não funciona, pois impede a criação de mais de um objeto, pois você está substituindo o método setattr da classe, o que significa que um segundo não pode ser criado como self.a = will falha na segunda inicialização.

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
fonte
1
Isso não vai funcionar: você está substituindo o método na classe , para obter NotImplementedError assim que tentar criar uma segunda instância.
slinkp
1
Se você deseja seguir essa abordagem, observe que é difícil substituir métodos especiais em tempo de execução: consulte stackoverflow.com/a/16426447/137635 para obter algumas soluções alternativas para isso.
slinkp
0

A solução básica abaixo aborda o seguinte cenário:

  • __init__() pode ser gravado acessando os atributos como de costume.
  • APÓS que o OBJECT esteja congelado apenas para alterações de atributos :

A idéia é substituir o __setattr__método e substituir sua implementação sempre que o status de congelamento do objeto for alterado.

Então, precisamos de algum método (_freeze ) que armazene essas duas implementações e alterne entre elas quando solicitado.

Esse mecanismo pode ser implementado dentro da classe de usuário ou herdado de uma Freezerclasse especial, como mostrado abaixo:

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
fonte
0

Assim como um dict

Eu tenho uma biblioteca de código aberto onde estou fazendo as coisas de maneira funcional , portanto, mover dados em um objeto imutável é útil. No entanto, não quero transformar meu objeto de dados para que o cliente interaja com eles. Então, eu vim com isso - ele fornece um ditado como objeto imutável + alguns métodos auxiliares.

Agradecemos a Sven Marnach em sua resposta pela implementação básica da restrição e atualização e exclusão de propriedades.

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 auxiliares

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)

Exemplos

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
fonte