Come realizzare un oggetto immutabile in Python?


181

Anche se non ne ho mai avuto bisogno, mi è sembrato solo che creare un oggetto immutabile in Python potesse essere leggermente complicato. Non puoi semplicemente sovrascrivere __setattr__, perché non puoi nemmeno impostare gli attributi in __init__. La sottoclasse di una tupla è un trucco che funziona:

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

Ma poi si ha accesso alla ae bvariabili attraverso la self[0]e self[1], che è fastidioso.

Questo è possibile in Pure Python? In caso contrario, come lo farei con un'estensione C?

(Le risposte che funzionano solo in Python 3 sono accettabili).

Aggiornare:

Quindi la tupla della sottoclasse è il modo di farlo in Pure Python, che funziona bene tranne che per l'ulteriore possibilità di accedere ai dati tramite [0], [1]ecc. Quindi, per completare questa domanda tutto ciò che manca è come farlo "correttamente" in C, che ho il sospetto che sarebbe molto semplice, semplicemente non effettui alcuna geititemo setattribute, ecc Ma invece di fare io stesso, offro una taglia per questo, perché sono pigro. :)


2
Il tuo codice non facilita l'accesso agli attributi tramite .ae .b? Questo è ciò per cui le proprietà sembrano esistere dopo tutto.
Sven Marnach,

1
@Sven Marnach: Sì, ma [0] e [1] funzionano ancora, e perché dovrebbero? Non li voglio. :) Forse l'idea di un oggetto immutabile con attributi non ha senso? :-)
Lennart Regebro,

2
Solo un'altra nota: NotImplementedè inteso solo come valore di ritorno per confronti approfonditi. Un valore di ritorno per __setatt__()è comunque piuttosto inutile, dal momento che di solito non lo vedrai affatto. Codice come immutable.x = 42silenziosamente non farà nulla. Dovresti TypeErrorinvece aumentare un .
Sven Marnach,

1
@Sven Marnach: OK, sono rimasto sorpreso, perché pensavo che potresti sollevare NotImplemented in questa situazione, ma questo dà uno strano errore. Così l'ho restituito invece, e sembrava funzionare. TypeError ha avuto un senso ovvio una volta che ho visto che l'hai usato.
Lennart Regebro,

1
@Lennart: potresti sollevare NotImplementedError, ma TypeErrorè ciò che genera una tupla se provi a modificarlo.
Sven Marnach,

Risposte:


115

Un'altra soluzione che ho appena pensato: il modo più semplice per ottenere lo stesso comportamento del codice originale è

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

Non risolve il problema che è possibile accedere agli attributi tramite [0]ecc., Ma almeno è considerevolmente più breve e offre l'ulteriore vantaggio di essere compatibile con picklee copy.

namedtuplecrea un tipo simile a quello che ho descritto in questa risposta , ovvero derivato tuplee utilizzato __slots__. È disponibile in Python 2.6 o versioni successive.


7
Il vantaggio di questa variante rispetto all'analogo scritto a mano (anche su Python 2.5 (l'utilizzo di verboseparametri per namedtupleil codice è facilmente generato)) è la singola interfaccia / implementazione di a namedtupleè preferibile a dozzine di interfacce / implementazioni scritte a mano così leggermente diverse che fare quasi la stessa cosa.
jfs,

2
OK, ottieni la "migliore risposta", perché è il modo più semplice per farlo. Sebastian ottiene la generosità per una breve implementazione di Cython. Saluti!
Lennart Regebro,

1
Un'altra caratteristica degli oggetti immutabili è che quando li si passa come parametro attraverso una funzione, vengono copiati per valore, piuttosto che un altro riferimento che viene fatto. Verrà namedtuplecopiato per valore quando viene passato attraverso le funzioni?
hlin117,

4
@ hlin117: ogni parametro viene passato come riferimento a un oggetto in Python, indipendentemente dal fatto che sia mutabile o immutabile. Per gli oggetti immutabili, sarebbe particolarmente inutile fare una copia - dal momento che non è possibile modificare l'oggetto comunque, si può anche passare un riferimento all'oggetto originale.
Sven Marnach,

Puoi usare namedtuple internamente all'interno della classe invece di creare un'istanza dell'oggetto esternamente? Sono molto nuovo in Python ma il vantaggio per l'altra tua risposta è che posso avere una classe per nascondere i dettagli e avere anche il potere di cose come parametri opzionali. Se guardo solo questa risposta, sembra che debba avere tutto ciò che usa la mia istanza di classe chiamata tuple. Grazie per entrambe le risposte.
Asaf,

78

Il modo più semplice per farlo è usare __slots__:

class A(object):
    __slots__ = []

Le istanze di Asono immutabili ora, poiché non è possibile impostare alcun attributo su di esse.

Se si desidera che le istanze della classe contengano dati, è possibile combinare questo con derivando da 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

Modifica : se vuoi sbarazzarti dell'indicizzazione, puoi ignorare __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

Si noti che non è possibile utilizzare operator.itemgetterper le proprietà in questo caso, poiché ciò farebbe Point.__getitem__()invece affidamento su tuple.__getitem__(). Inoltre, ciò non impedirà l'utilizzo di tuple.__getitem__(p, 0), ma non riesco quasi a immaginare come questo dovrebbe costituire un problema.

Non credo che il modo "giusto" di creare un oggetto immutabile sia scrivere un'estensione C. Python di solito fa affidamento sul fatto che gli implementatori e gli utenti delle biblioteche siano adulti consenzienti e, invece di imporre davvero un'interfaccia, l'interfaccia dovrebbe essere chiaramente indicata nella documentazione. Questo è il motivo per cui non considero la possibilità di aggirare un scavalcato __setattr__()chiamando object.__setattr__()un problema. Se qualcuno lo fa, è a proprio rischio.


1
Non sarebbe un'idea migliore usare un tuplequi __slots__ = (), piuttosto che __slots__ = []? (Solo chiarendo)
user225312

1
@sukhbir: penso che questo non abbia alcuna importanza. Perché preferiresti una tupla?
Sven Marnach,

1
@Sven: sono d'accordo che non importerebbe (tranne la parte della velocità, che possiamo ignorare), ma ci ho pensato in questo modo: __slots__non cambierà, vero? Lo scopo è identificare per una volta quali attributi possono essere impostati. Quindi non tuplesembra una scelta molto naturale in questo caso?
user225312

5
Ma con un vuoto __slots__non posso impostare alcun attributo. E se ho __slots__ = ('a', 'b')quindi gli attributi aeb sono ancora mutabili.
Lennart Regebro,

Ma la tua soluzione è migliore dell'override, __setattr__quindi è un miglioramento rispetto alla mia. +1 :)
Lennart Regebro,

50

..come farlo "correttamente" in C ..

È possibile utilizzare Cython per creare un tipo di estensione per 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

Funziona con Python 2.xe 3.

test

# 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 non ti dispiace supportare l'indicizzazione, allora collections.namedtuplesuggerito da @Sven Marnach è preferibile:

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

@Lennart: le istanze di namedtuple(o più precisamente del tipo restituito dalla funzione namedtuple()) sono immutabili. Decisamente.
Sven Marnach,

@Lennart Regebro: namedtuplesupera tutti i test (tranne il supporto di indicizzazione). Quale requisito ho perso?
jfs

Sì, hai ragione, ho creato un tipo namedtuple, l'ho istanziato e quindi ho eseguito il test sul tipo anziché sull'istanza. Eh. :-)
Lennart Regebro,

posso chiedere perché si dovrebbe avere un riferimento debole qui?
McSinyx,

1
@McSinyx: in caso contrario, gli oggetti non possono essere utilizzati nelle raccolte di deboli. Cosa c'è esattamente __weakref__in Python?
jfs il

40

Un'altra idea sarebbe quella di non consentire __setattr__e utilizzare completamente object.__setattr__nel costruttore:

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

Naturalmente è possibile utilizzare object.__setattr__(p, "x", 3)per modificare Pointun'istanza p, ma l'implementazione originale soffre dello stesso problema (provare tuple.__setattr__(i, "x", 42)su Immutableun'istanza).

È possibile applicare lo stesso trucco nell'implementazione originale: eliminare __getitem__()e utilizzare tuple.__getitem__()nelle funzioni di proprietà.


11
Non mi interesserebbe che qualcuno modificasse deliberatamente l'oggetto usando la superclasse ' __setattr__, perché il punto non deve essere infallibile. Il punto è chiarire che non deve essere modificato e impedire modifiche per errore.
zvone,

18

È possibile creare un @immutabledecoratore che sovrascrive __setattr__ e cambia __slots__in un elenco vuoto, quindi decorare il __init__metodo con esso.

Modifica: come notato dall'OP, la modifica __slots__dell'attributo impedisce solo la creazione di nuovi attributi , non la modifica.

Modifica2: ecco un'implementazione:

Edit3: l'utilizzo __slots__rompe questo codice, perché se interrompe la creazione dell'oggetto __dict__. Sto cercando un'alternativa.

Edit4: Beh, questo è tutto. È un gioco da ragazzi, ma funziona come un esercizio :-)

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

1
Realizzare un decoratore (classe?) O una metaclasse dalla soluzione è davvero una buona idea, ma la domanda è: qual è la soluzione. :)
Lennart Regebro,

3
object.__setattr__()lo rompe stackoverflow.com/questions/4828080/…
jfs

Infatti. Ho appena svolto un esercizio sui decoratori.
PaoloVictor

13

Usando una Dataclass congelata

Per Python 3.7+ puoi usare una Classe di dati con frozen=Trueun'opzione , che è un modo molto pitone e mantenibile per fare ciò che vuoi.

Sembrerebbe qualcosa del genere:

from dataclasses import dataclass

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

Dato che per i campi delle dataclass è richiesto un suggerimento sul tipo , ho usato Any dal typingmodulo .

Motivi per NON utilizzare una Namedtuple

Prima di Python 3.7 era frequente vedere le denominazioni delle coppie usate come oggetti immutabili. Può essere complicato in molti modi, uno di questi è che il __eq__metodo tra namedtuples non considera le classi degli oggetti. Per esempio:

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

Come vedi, anche se i tipi di obj1e obj2sono diversi, anche se i nomi dei loro campi sono diversi, obj1 == obj2continua a dare True. Questo perché il __eq__metodo utilizzato è quello della tupla, che confronta solo i valori dei campi dati le loro posizioni. Questa può essere un'enorme fonte di errori, specialmente se stai eseguendo la sottoclasse di queste classi.


10

Non penso che sia del tutto possibile se non usando una tupla o una tupla nominata. In ogni caso, se si esegue __setattr__()l' override, l'utente può sempre ignorarlo chiamando object.__setattr__()direttamente. Qualsiasi soluzione da cui dipende __setattr__è garantita per non funzionare.

Quanto segue è il più vicino che puoi ottenere senza usare una sorta di 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__

ma si rompe se ci provi abbastanza:

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

ma l'uso di Sven namedtupleè davvero immutabile.

Aggiornare

Poiché la domanda è stata aggiornata per chiedere come farlo correttamente in C, ecco la mia risposta su come farlo correttamente in Cython:

Primo 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.pyper compilarlo (usando il 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
)

Quindi per provarlo:

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

Grazie per il codice Cython, Cython è fantastico. L'implementazione di JF Sebastians con il readonly è più ordinata ed è arrivata per prima, quindi ottiene la taglia.
Lennart Regebro,

5

Ho creato classi immutabili sovrascrivendo __setattr__e consentendo il set se il chiamante è __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)

Questo non è ancora abbastanza, dal momento che consente a chiunque di ___init__cambiare l'oggetto, ma si ottiene l'idea.



3
L'uso del controllo dello stack per assicurarsi che il chiamante __init__non sia molto soddisfacente.
gb.

5

Oltre alle eccellenti altre risposte, mi piace aggiungere un metodo per Python 3.4 (o forse 3.3). Questa risposta si basa su diverse risposte precedenti a questa domanda.

In python 3.4, puoi usare le proprietà senza setter per creare membri della classe che non possono essere modificati. (Nelle versioni precedenti era possibile assegnare a proprietà senza setter.)

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

Puoi usarlo in questo modo:

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

che stamperà "constant"

Ma la chiamata instance.a=10causerà:

AttributeError: can't set attribute

Spiegazione: le proprietà senza setter sono una caratteristica molto recente di Python 3.4 (e penso 3.3). Se si tenta di assegnare a tale proprietà, verrà generato un errore. Usando gli slot, limito le variabili membro a __A_a(che è __a).

Problema: l'assegnazione a _A__aè ancora possibile ( instance._A__a=2). Ma se si assegna a una variabile privata, è colpa tua ...

Questa risposta tra l'altro, tuttavia, scoraggia l'uso di __slots__. L'uso di altri modi per impedire la creazione di attributi potrebbe essere preferibile.


propertyè disponibile anche su Python 2 (guarda il codice nella domanda stessa). Non crea un oggetto immutabile, prova i test dalla mia risposta , ad esempio, instance.b = 1crea un nuovo battributo.
jfs,

Bene, la domanda è davvero come impedire di fare, A().b = "foo"cioè non consentire l'impostazione di nuovi attributi.
Lennart Regebro,

La proprietà corretta senza setter genera un errore in Python 3.4 se si tenta di assegnare a quella proprietà. Nelle versioni precedenti il ​​setter veniva generato implicitamente.
Bernhard,

@Lennart: la mia soluzione è una risposta a un sottoinsieme di casi d'uso per oggetti immutabili e un'aggiunta alle risposte precedenti. Uno dei motivi per cui potrei desiderare un oggetto immutabile è che posso renderlo hash, per cui la mia soluzione potrebbe funzionare. Ma hai ragione, questo non è un oggetto immutabile.
Bernhard,

@ jf-sebastian: ho cambiato la mia risposta per utilizzare gli slot per impedire la creazione di attributi. La novità della mia risposta rispetto ad altre risposte è che uso le proprietà di python3.4 per evitare di modificare gli attributi esistenti. Mentre lo stesso si ottiene nelle risposte precedenti, il mio codice è più breve a causa del cambiamento nel comportamento delle proprietà.
Bernhard,

5

Ecco una soluzione 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))

Eredita da questa classe, inizializza i tuoi campi nel costruttore e il gioco è fatto.


1
ma con questa logica è possibile assegnare nuovi attributi all'oggetto
eseguito il

3

Se sei interessato ad oggetti con comportamento, allora nametuple è quasi la tua soluzione.

Come descritto in fondo alla documentazione di namedtuple , puoi derivare la tua classe da namedtuple; e quindi, è possibile aggiungere il comportamento desiderato.

Ad esempio (codice prelevato direttamente dalla documentazione ):

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)

Ciò comporterà:

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

Questo approccio funziona sia per Python 3 che per Python 2.7 (testato anche su IronPython).
L'unico aspetto negativo è che l'albero delle eredità è un po 'strano; ma questo non è qualcosa con cui giochi abitualmente.


1
Python 3.6+ lo supporta direttamente, usandoclass Point(typing.NamedTuple):
Elazar

3

Le classi che ereditano dalla seguente Immutableclasse sono immutabili, così come le loro istanze, al __init__termine dell'esecuzione del metodo. Dal momento che è puro pitone, come altri hanno sottolineato, non c'è nulla che impedisce a qualcuno di utilizzare i metodi speciali mutanti dalla base objecte type, ma questo è sufficiente per impedire a chiunque di mutare accidentalmente una classe / istanza.

Funziona dirottando il processo di creazione della classe con una 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__')

2

Ne avevo bisogno poco fa e ho deciso di creare un pacchetto Python per questo. La versione iniziale è ora su PyPI:

$ pip install immutable

Usare:

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

Documenti completi qui: https://github.com/theengineear/immutable

Spero che aiuti, avvolge una coppia nominata come è stato discusso, ma rende l'istanza molto più semplice.


2

In questo modo non smette object.__setattr__di funzionare, ma l'ho ancora trovato utile:

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)

potrebbe essere necessario sostituire più elementi (come __setitem__) in base al caso d'uso.


Ho trovato qualcosa di simile prima di vederlo, ma usato in getattrmodo da poter fornire un valore predefinito per frozen. Ciò ha semplificato un po 'le cose. stackoverflow.com/a/22545808/5987
Mark Ransom

Mi piace questo approccio al meglio, ma non è necessario l' __new__override. All'interno __setattr__basta sostituire il condizionale conif name != '_frozen' and getattr(self, "_frozen", False)
Pete Cacioppi

Inoltre, non è necessario congelare la classe al momento della costruzione. Puoi bloccarlo in qualsiasi momento se fornisci una freeze()funzione. L'oggetto verrà quindi "congelato una volta". Infine, preoccuparsi object.__setattr__è sciocco, perché "siamo tutti adulti qui".
Pete Cacioppi,

2

A partire da Python 3.7, puoi usare il @dataclassdecoratore nella tua classe e sarà immutabile come una struttura! Tuttavia, può aggiungere o meno un __hash__()metodo alla tua classe. Citazione:

hash () viene utilizzato da hash () incorporato e quando gli oggetti vengono aggiunti a raccolte hash come dizionari e set. Avere un hash () implica che le istanze della classe sono immutabili. La mutabilità è una proprietà complicata che dipende dall'intento del programmatore, dall'esistenza e dal comportamento di eq () e dai valori di eq e flag congelati nel decoratore di dataclass ().

Per impostazione predefinita, dataclass () non aggiungerà implicitamente un metodo hash () a meno che non sia sicuro farlo. Né aggiungerà o modificherà un metodo hash () esplicitamente definito esistente . L'impostazione dell'attributo class hash = None ha un significato specifico per Python, come descritto nella documentazione di hash ().

Se hash () non è definito esplicitamente o se è impostato su Nessuno, dataclass () può aggiungere un metodo hash () implicito . Sebbene non sia raccomandato, è possibile forzare dataclass () per creare un metodo hash () con unsafe_hash = True. Questo potrebbe essere il caso se la tua classe è logicamente immutabile ma può comunque essere mutata. Questo è un caso d'uso specializzato e deve essere considerato attentamente.

Ecco l'esempio dai documenti collegati sopra:

@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

1
devi usare frozen, cioè @dataclass(frozen=True), ma sostanzialmente blocca l'uso di __setattr__e __delattr__come nella maggior parte delle altre risposte qui. Lo fa in un modo compatibile con le altre opzioni di dataclass.
CS

2

Puoi sovrascrivere setattr e continuare a usare init per impostare la variabile. Utilizzeresti setattr super class . ecco il codice.

classe Immutabile:
    __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, * ignored):
        raise NotImplementedError

    def __delattr __ (self, * ignored):
        raise NotImplementedError

O semplicemente passinvece diraise NotImplementedError
jonathan.scholbach

Non è affatto una buona idea fare "pass" in __setattr__ e __delattr__ in questo caso. Il semplice motivo è che se qualcuno assegna un valore a un campo / proprietà, si aspettano naturalmente che il campo venga modificato. Se vuoi seguire il percorso della "minima sorpresa" (come dovresti), devi segnalare un errore. Ma non sono sicuro che NotImplementedError sia quello giusto da sollevare. Sollevo qualcosa del tipo "Campo / proprietà è immutabile". errore ... Penso che debba essere generata un'eccezione personalizzata.
darlove,

1

Il attrmodulo di terze parti fornisce questa funzionalità .

Modifica: python 3.7 ha adottato questa idea nello 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 le classi congelate eseguendo l'override __setattr__ e ha un impatto minore sulle prestazioni ad ogni istante, secondo la documentazione.

Se hai l'abitudine di usare le classi come tipi di dati, attrpuò essere particolarmente utile in quanto si occupa della piastra della caldaia per te (ma non fa alcuna magia). In particolare, scrive nove metodi dunder (__X__) per te (a meno che tu non li disattivi), tra cui repr, init, hash e tutte le funzioni di confronto.

attrfornisce anche un aiuto per__slots__ .


1

Quindi, sto scrivendo rispettivamente di Python 3:

I) con l'aiuto del decoratore della classe di dati e impostare frozen = True. possiamo creare oggetti immutabili in Python.

per questo è necessario importare la classe di dati dalla lib di classi di dati e deve impostare frozen = True

ex.

da dataclass importare dataclass

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

operazione:

l = Location ("Delhi", 112.345, 234.788) l.name 'Delhi' l.longitude 112.345 l.latitude 234.788 l.name = "Kolkata" dataclasses.FrozenInstanceError: impossibile assegnare al campo 'name'

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


0

Un approccio alternativo è quello di creare un wrapper che renda immutabile un'istanza.

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)

Ciò è utile in situazioni in cui solo alcune istanze devono essere immutabili (come gli argomenti predefiniti delle chiamate di funzione).

Può essere utilizzato anche in fabbriche immutabili come:

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

Protegge anche da object.__setattr__, ma si adatta ad altri trucchi a causa della natura dinamica di Python.


0

Ho usato la stessa idea di Alex: una meta-classe e un "iniziatore", ma in combinazione con la sovrascrittura __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: sto chiamando direttamente la meta-classe per farla funzionare sia per 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.

Funziona anche con gli slot ...:

>>> 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 eredità multipla:

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

Si noti, tuttavia, che gli attributi mutabili rimangono mutabili:

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

0

Una cosa che non è davvero inclusa qui è l'immutabilità totale ... non solo l'oggetto genitore, ma anche tutti i bambini. tuple / frozenset possono essere immutabili per esempio, ma gli oggetti di cui fa parte potrebbero non esserlo. Ecco una versione piccola (incompleta) che fa un buon lavoro nel far rispettare l'immutabilità fino in fondo:

# 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

Puoi semplicemente sostituire setAttr nella dichiarazione finale di init. Quindi puoi costruire ma non cambiare. Ovviamente puoi ancora scavalcare l'oggetto usint. setAttr, ma in pratica la maggior parte delle lingue ha una qualche forma di riflessione, quindi l'immutabilità è sempre un'astrazione che perde. L'immutabilità riguarda soprattutto la prevenzione da parte dei clienti della violazione accidentale del contratto di un oggetto. Io uso:

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

La soluzione originale offerta era errata, questa è stata aggiornata in base ai commenti usando la soluzione da qui

La soluzione originale è sbagliata in modo interessante, quindi è inclusa nella parte inferiore.

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

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

Produzione :

1
2
Attempted To Modify Immutable Object
1
2

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

Implementazione originale:

È stato sottolineato nei commenti, correttamente, che questo in realtà non funziona, poiché impedisce la creazione di più di un oggetto mentre si sovrascrive il metodo setattr di classe, il che significa che un secondo non può essere creato come self.a = will fallire alla seconda inizializzazione.

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

1
Non funzionerà: stai sovrascrivendo il metodo sulla classe , quindi otterrai NotImplementedError non appena provi a creare una seconda istanza.
slinkp

1
Se si desidera perseguire questo approccio, tenere presente che è difficile ignorare i metodi speciali in fase di esecuzione: consultare stackoverflow.com/a/16426447/137635 per un paio di soluzioni alternative.
slinkp

0

La soluzione di base seguente risolve il seguente scenario:

  • __init__() può essere scritto accedendo agli attributi come al solito.
  • DOPO che OBJECT è bloccato solo per le modifiche agli attributi :

L'idea è quella di sovrascrivere il __setattr__metodo e sostituirne l'implementazione ogni volta che lo stato di oggetto congelato viene modificato.

Quindi abbiamo bisogno di un metodo ( _freeze) che memorizzi queste due implementazioni e commuti tra loro quando richiesto.

Questo meccanismo può essere implementato all'interno della classe utente o ereditato da una Freezerclasse speciale come mostrato di seguito:

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

Proprio come a dict

Ho una libreria open source in cui sto facendo le cose in modo funzionale , quindi è utile spostare i dati in un oggetto immutabile. Tuttavia, non voglio dover trasformare il mio oggetto dati affinché il client possa interagire con loro. Quindi, mi è venuto in mente questo: ti dà un oggetto simile a quello che è immutabile + alcuni metodi di supporto.

Ringraziamo Sven Marnach nella sua risposta per l'implementazione di base della limitazione dell'aggiornamento e dell'eliminazione della proprietà.

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__

Metodi di supporto

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)

Esempi

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.