Wie erstelle ich ein unveränderliches Objekt in Python?

179

Obwohl ich das nie gebraucht habe, ist mir nur aufgefallen, dass das Erstellen eines unveränderlichen Objekts in Python etwas schwierig sein kann. Sie können nicht einfach überschreiben __setattr__, da Sie dann nicht einmal Attribute in der festlegen können __init__. Das Unterklassen eines Tupels ist ein Trick, der funktioniert:

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

Aber dann haben Sie Zugriff auf die aund bVariablen durch self[0]und self[1], was ärgerlich ist.

Ist das in Pure Python möglich? Wenn nicht, wie würde ich das mit einer C-Erweiterung machen?

(Antworten, die nur in Python 3 funktionieren, sind akzeptabel).

Aktualisieren:

So Subklassen Tupel ist so , wie es in reinen Python zu tun, die die Daten für den Zugriff durch gut mit Ausnahme der zusätzlichen Möglichkeit funktioniert [0], [1]etc. Also, zu vervollständigen diese Frage alles , was fehlt , ist es Howto zu tun „richtig“ in C, die Ich vermute, es wäre ganz einfach, einfach kein geititemoder setattributeusw. zu implementieren . Aber anstatt es selbst zu tun, biete ich dafür ein Kopfgeld an, weil ich faul bin. :) :)

Lennart Regebro
quelle
2
Erleichtert Ihr Code nicht den Zugriff auf die Attribute über .aund .b? Dafür scheinen die Eigenschaften doch zu existieren.
Sven Marnach
1
@Sven Marnach: Ja, aber [0] und [1] funktionieren immer noch und warum sollten sie? Ich will sie nicht. :) Vielleicht ist die Idee eines unveränderlichen Objekts mit Attributen Unsinn? :-)
Lennart Regebro
2
Nur ein weiterer Hinweis: NotImplementedist nur als Rückgabewert für umfangreiche Vergleiche gedacht. Ein Rückgabewert für __setatt__()ist sowieso ziemlich sinnlos, da Sie ihn normalerweise überhaupt nicht sehen. Code wie immutable.x = 42wird stillschweigend nichts tun. Sie sollten TypeErrorstattdessen eine erhöhen .
Sven Marnach
1
@Sven Marnach: OK, ich war überrascht, weil ich dachte, Sie könnten NotImplemented in dieser Situation auslösen, aber das gibt einen seltsamen Fehler. Also gab ich es stattdessen zurück und es schien zu funktionieren. TypeError machte offensichtlich Sinn, als ich sah, dass Sie es verwendet haben.
Lennart Regebro
1
@ Lennart: Sie könnten erhöhen NotImplementedError, aber TypeErrorwas ist ein Tupel, wenn Sie versuchen, es zu ändern.
Sven Marnach

Antworten:

115

Eine weitere Lösung, an die ich gerade gedacht habe: Der einfachste Weg, das gleiche Verhalten wie bei Ihrem ursprünglichen Code zu erzielen, ist

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

Es löst nicht das Problem, dass auf Attribute über [0]usw. zugegriffen werden kann , aber es ist zumindest erheblich kürzer und bietet den zusätzlichen Vorteil, mit pickleund kompatibel zu sein copy.

namedtupleerstellt einen Typ ähnlich dem, was ich in dieser Antwort beschrieben habe , dh abgeleitet von tupleund unter Verwendung __slots__. Es ist in Python 2.6 oder höher verfügbar.

Sven Marnach
quelle
7
Der Vorteil dieser Variante gegenüber handgeschriebenem Analog (selbst unter Python 2.5 (die Verwendung von verboseParametern für namedtupleden Code lässt sich leicht generieren)) besteht darin, dass die einzelne Schnittstelle / Implementierung von a namedtupleDutzenden von leicht unterschiedlichen handgeschriebenen Schnittstellen / Implementierungen vorzuziehen ist mach fast das gleiche.
JFS
2
OK, Sie erhalten die "beste Antwort", weil dies der einfachste Weg ist. Sebastian bekommt das Kopfgeld für eine kurze Cython-Implementierung. Prost!
Lennart Regebro
1
Ein weiteres Merkmal unveränderlicher Objekte ist, dass sie, wenn Sie sie als Parameter durch eine Funktion übergeben, nach Wert kopiert werden und nicht nach einer anderen Referenz. Würden namedtuples nach Wert kopiert, wenn sie durch Funktionen geleitet werden?
hlin117
4
@ hlin117: Jeder Parameter wird als Referenz auf ein Objekt in Python übergeben, unabhängig davon, ob es veränderlich oder unveränderlich ist. Bei unveränderlichen Objekten wäre es besonders sinnlos, eine Kopie zu erstellen. Da Sie das Objekt ohnehin nicht ändern können, können Sie auch einen Verweis auf das Originalobjekt übergeben.
Sven Marnach
Können Sie namedtuple intern innerhalb der Klasse verwenden, anstatt das Objekt extern zu instanziieren? Ich bin sehr neu in Python, aber der Vorteil Ihrer anderen Antwort ist, dass ich eine Klasse die Details verbergen lassen kann und auch die Macht von Dingen wie optionalen Parametern habe. Wenn ich nur diese Antwort betrachte, scheint es, als müsste ich alles haben, was meine Klasse verwendet, um Tupel zu instanziieren. Vielen Dank für beide Antworten.
Asaf
78

Der einfachste Weg, dies zu tun, ist __slots__:

class A(object):
    __slots__ = []

Instanzen von Asind jetzt unveränderlich, da Sie ihnen keine Attribute zuweisen können.

Wenn die Klasseninstanzen Daten enthalten sollen, können Sie dies mit folgenden Ergebnissen kombinieren 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

Bearbeiten : Wenn Sie die Indizierung ebenfalls entfernen möchten, können Sie Folgendes überschreiben __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

Beachten Sie, dass Sie operator.itemgetterin diesem Fall nicht für die Eigenschaften verwenden können, da dies Point.__getitem__()anstelle von tuple.__getitem__(). Darüber hinaus wird dies die Verwendung von nicht verhindern tuple.__getitem__(p, 0), aber ich kann mir kaum vorstellen, wie dies ein Problem darstellen sollte.

Ich denke nicht, dass der "richtige" Weg, ein unveränderliches Objekt zu erstellen, darin besteht, eine C-Erweiterung zu schreiben. Python ist normalerweise darauf angewiesen, dass Bibliotheksimplementierer und Bibliotheksbenutzer Erwachsenen zustimmen. Anstatt eine Schnittstelle wirklich durchzusetzen, sollte die Schnittstelle in der Dokumentation klar angegeben werden. Aus diesem Grund denke ich nicht über die Möglichkeit nach, ein Überschreiben __setattr__()durch Aufrufen object.__setattr__()eines Problems zu umgehen . Wenn jemand dies tut, geschieht dies auf eigenes Risiko.

Sven Marnach
quelle
1
Wäre es nicht eine bessere Idee, ein tuplehier zu verwenden ?__slots__ = () als __slots__ = []? (Nur zur Klarstellung)
user225312
1
@sukhbir: Ich denke, das spielt überhaupt keine Rolle. Warum bevorzugen Sie ein Tupel?
Sven Marnach
1
@Sven: Ich bin damit einverstanden, dass es keine Rolle spielt (außer dem Geschwindigkeitsteil, den wir ignorieren können), aber ich habe es so gesehen: Wird es nicht __slots__richtig geändert? Ziel ist es, einmal zu identifizieren, welche Attribute festgelegt werden können. tupleScheint a in einem solchen Fall nicht eine ganz natürliche Wahl zu sein?
user225312
5
Aber mit einem leeren __slots__kann ich keine Attribute setzen. Und wenn ja, __slots__ = ('a', 'b')dann sind die Attribute a und b immer noch veränderlich.
Lennart Regebro
Aber Ihre Lösung ist besser als das Überschreiben, __setattr__also ist es eine Verbesserung gegenüber meiner. +1 :)
Lennart Regebro
50

..wie mache ich es "richtig" in C ..

Mit Cython können Sie einen Erweiterungstyp für Python erstellen:

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

Es funktioniert sowohl mit Python 2.x als auch mit 3.

Tests

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

Wenn Ihnen die Indizierungsunterstützung nichts ausmacht, ist der collections.namedtupleVorschlag von @Sven Marnach vorzuziehen:

Immutable = collections.namedtuple("Immutable", "a b")
jfs
quelle
@Lennart: Instanzen von namedtuple(oder genauer gesagt von dem von der Funktion zurückgegebenen Typ namedtuple()) sind unveränderlich. Bestimmt.
Sven Marnach
@ Lennart Regebro: Besteht namedtuplealle Tests (außer Indizierungsunterstützung). Welche Anforderung habe ich vermisst?
JFS
Ja, Sie haben Recht, ich habe einen Namenstupeltyp erstellt, ihn instanziiert und dann den Test für den Typ anstelle der Instanz durchgeführt. Heh. :-)
Lennart Regebro
Darf ich fragen, warum man hier eine schwache Referenzierung braucht?
McSinyx
1
@McSinyx: Andernfalls können die Objekte nicht in den Sammlungen von Schwachref verwendet werden. Was genau ist __weakref__in Python?
JFS
40

Eine andere Idee wäre, den Konstruktor vollständig zu verbieten __setattr__und zu verwenden object.__setattr__:

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

Natürlich können Sie object.__setattr__(p, "x", 3)eine PointInstanz ändern p, aber Ihre ursprüngliche Implementierung leidet unter demselben Problem (versuchen Sie estuple.__setattr__(i, "x", 42) eine ImmutableInstanz aus).

Sie können denselben Trick in Ihrer ursprünglichen Implementierung anwenden: Entfernen __getitem__()und Verwenden tuple.__getitem__()in Ihren Eigenschaftsfunktionen.

Sven Marnach
quelle
11
Es würde mich nicht interessieren, wenn jemand das Objekt absichtlich mit Superklasse modifiziert __setattr__, weil es nicht darum geht, narrensicher zu sein. Es geht darum, klar zu machen, dass es nicht geändert werden sollte, und eine versehentliche Änderung zu verhindern.
Zvone
18

Sie können einen @immutableDekorator erstellen , der entweder die überschreibt __setattr__ und die __slots__Liste in eine leere Liste ändert , und dann die dekorieren__init__ Methode damit .

Bearbeiten: Wie das OP feststellte, __slots__verhindert das Ändern des Attributs nur die Erstellung neuer Attribute , nicht die Änderung.

Edit2: Hier ist eine Implementierung:

Edit3: Using unterbricht __slots__diesen Code, da if die Erstellung des Objekts stoppt__dict__ . Ich suche eine Alternative.

Edit4: Nun, das ist es. Es ist zwar hackisch, funktioniert aber als Übung :-)

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
quelle
1
Es ist zwar eine gute Idee, aus der Lösung einen (Klassen-?) Dekorateur oder eine Metaklasse zu machen, aber die Frage ist, was die Lösung ist. :)
Lennart Regebro
3
object.__setattr__()bricht es stackoverflow.com/questions/4828080/…
jfs
Tatsächlich. Ich habe gerade als Übung über Dekorateure weitergemacht.
PaoloVictor
11

Verwenden einer eingefrorenen Datenklasse

Für Python 3.7+ können Sie verwenden Datenklasse mit einer frozen=TrueOption , die ein sehr pythonic und wartbar Weg ist , zu tun , was Sie wollen.

Es würde ungefähr so ​​aussehen:

from dataclasses import dataclass

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

Da für die Felder der Datenklassen Typhinweise erforderlich sind, habe ich Any aus dem typingModul verwendet .

Gründe, ein Namedtuple NICHT zu verwenden

Vor Python 3.7 wurde häufig festgestellt, dass Namedtuples als unveränderliche Objekte verwendet wurden. Es kann in vielerlei Hinsicht schwierig sein. Eine davon ist, dass die __eq__Methode zwischen den benannten Tupeln die Klassen der Objekte nicht berücksichtigt. Beispielsweise:

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

Wie Sie sehen, auch wenn die Typen obj1und obj2unterschiedlich sind, auch wenn ihre Felder die Namen unterschiedlich sind, obj1 == obj2gibt nach wie vor True. Dies liegt daran, dass die verwendete __eq__Methode die des Tupels ist, bei der nur die Werte der Felder anhand ihrer Position verglichen werden. Dies kann eine große Fehlerquelle sein, insbesondere wenn Sie diese Klassen in Unterklassen unterteilen.

Jundiaius
quelle
10

Ich denke nicht, dass es völlig möglich ist, außer wenn entweder ein Tupel oder ein benanntes Tupel verwendet wird. Egal was passiert, wenn Sie überschreiben, kann __setattr__()der Benutzer es immer umgehen, indem er object.__setattr__()direkt anruft . Jede Lösung, die davon abhängt __setattr__, funktioniert garantiert nicht.

Das Folgende ist ungefähr das nächste, das Sie erhalten können, ohne ein Tupel zu verwenden:

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__

aber es bricht, wenn Sie sich genug anstrengen:

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

aber Svens Gebrauch von namedtupleist wirklich unveränderlich.

Aktualisieren

Da die Frage aktualisiert wurde, um zu fragen, wie es in C richtig gemacht wird, ist hier meine Antwort, wie man es in Cython richtig macht:

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

und a setup.py, um es zu kompilieren (mit dem Befehl 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
)

Dann probieren Sie es aus:

>>> 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
quelle
Vielen Dank für den Cython-Code, Cython ist großartig. JF Sebastians Implementierung mit dem Readonly ist ordentlicher und kam zuerst an, also bekommt er das Kopfgeld.
Lennart Regebro
5

Ich habe unveränderliche Klassen erstellt, indem ich sie überschrieben __setattr__und die Menge zugelassen habe, wenn der Aufrufer __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)

Dies ist noch nicht genug, da es jedem erlaubt, ___init__das Objekt zu ändern, aber Sie bekommen die Idee.

Ned Batchelder
quelle
object.__setattr__()bricht es stackoverflow.com/questions/4828080/…
jfs
3
Die Verwendung der Stapelinspektion, um sicherzustellen, dass der Anrufer __init__zufriedenstellend ist, ist nicht sehr zufriedenstellend.
gb.
5

Zusätzlich zu den hervorragenden anderen Antworten möchte ich eine Methode für Python 3.4 (oder vielleicht 3.3) hinzufügen. Diese Antwort baut auf mehreren vorherigen Antworten auf diese Frage auf.

In Python 3.4 können Sie Eigenschaften ohne Setter verwenden , um Klassenmitglieder zu erstellen, die nicht geändert werden können. (In früheren Versionen war die Zuweisung von Eigenschaften ohne Setter möglich.)

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

Sie können es so verwenden:

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

welches drucken wird "constant"

Aber das Anrufen instance.a=10wird verursachen:

AttributeError: can't set attribute

Erklärung: Eigenschaften ohne Setter sind ein sehr neues Merkmal von Python 3.4 (und ich denke 3.3). Wenn Sie versuchen, einer solchen Eigenschaft zuzuweisen, wird ein Fehler ausgelöst. Mit Slots beschränke ich die Mitgliedsvariablen auf __A_a(was ist __a).

Problem: Eine Zuordnung zu _A__aist weiterhin möglich ( instance._A__a=2). Aber wenn Sie einer privaten Variablen zuweisen, ist es Ihre eigene Schuld ...

Diese Antwort rät jedoch unter anderem von der Verwendung von ab __slots__. Die Verwendung anderer Methoden zur Verhinderung der Attributerstellung ist möglicherweise vorzuziehen.

Bernhard
quelle
propertyist auch in Python 2 verfügbar (siehe Code in der Frage selbst). Es wird kein unveränderliches Objekt erstellt, versuchen Sie die Tests aus meiner Antwort, z. B. instance.b = 1erstellt ein neues bAttribut.
JFS
Richtig, die Frage ist wirklich, wie man dies verhindert, A().b = "foo"dh das Festlegen neuer Attribute nicht zulässt.
Lennart Regebro
Eigenschaften ohne Setter lösen in Python 3.4 einen Fehler aus, wenn Sie versuchen, dieser Eigenschaft zuzuweisen. In früheren Versionen wurde der Setter implizit generiert.
Bernhard
@ Lennart: Meine Lösung ist eine Antwort auf eine Teilmenge von Anwendungsfällen für unveränderliche Objekte und eine Ergänzung zu früheren Antworten. Ein Grund, warum ich ein unveränderliches Objekt haben möchte, ist, dass ich es hashbar machen kann. In diesem Fall funktioniert meine Lösung möglicherweise. Aber Sie haben Recht, dies ist kein unveränderliches Objekt.
Bernhard
@ jf-sebastian: Meine Antwort wurde geändert, um Slots zu verwenden, um die Erstellung von Attributen zu verhindern. Neu in meiner Antwort im Vergleich zu anderen Antworten ist, dass ich die Eigenschaften von python3.4 verwende, um zu vermeiden, dass vorhandene Attribute geändert werden. Während das Gleiche in den vorherigen Antworten erreicht wird, ist mein Code aufgrund der Änderung des Verhaltens von Eigenschaften kürzer.
Bernhard
5

Hier ist eine elegante Lösung:

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

Erben Sie diese Klasse, initialisieren Sie Ihre Felder im Konstruktor, und schon sind Sie fertig.

Alexander Ryzhov
quelle
1
aber mit dieser Logik ist es möglich, dem Objekt neue Attribute zuzuweisen
javed
3

Wenn Sie sich für Objekte mit Verhalten interessieren, ist namedtuple fast Ihre Lösung.

Wie am Ende der Dokumentation zu namedtuple beschrieben , können Sie Ihre eigene Klasse von namedtuple ableiten. Anschließend können Sie das gewünschte Verhalten hinzufügen.

Zum Beispiel (Code direkt aus der Dokumentation entnommen ):

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)

Dies führt zu:

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

Dieser Ansatz funktioniert sowohl für Python 3 als auch für Python 2.7 (auch auf IronPython getestet).
Der einzige Nachteil ist, dass der Vererbungsbaum etwas seltsam ist. Aber damit spielt man normalerweise nicht.

rauben
quelle
1
Python 3.6+ unterstützt dies direkt mitclass Point(typing.NamedTuple):
Elazar
3

Klassen, die von der folgenden ImmutableKlasse erben , sind ebenso wie ihre Instanzen unveränderlich, nachdem die __init__Ausführung ihrer Methode abgeschlossen ist. Da es sich, wie andere bereits betont haben, um reines Python handelt, hindert nichts jemanden daran, die mutierenden Spezialmethoden von der Basis aus zu verwenden, objectund typedies reicht aus, um zu verhindern, dass jemand versehentlich eine Klasse / Instanz mutiert.

Es funktioniert, indem der Klassenerstellungsprozess mit einer Metaklasse entführt wird.

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

Ich brauchte das vor einiger Zeit und beschloss, ein Python-Paket dafür zu erstellen. Die erste Version ist jetzt auf PyPI:

$ pip install immutable

Benutzen:

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

Vollständige Dokumente hier: https://github.com/theengineear/immutable

Ich hoffe, es hilft, es umschließt ein benanntes Tupel, wie es bereits besprochen wurde, macht aber die Instanziierung viel einfacher.

theengineear
quelle
2

Dieser Weg hört nicht auf zu object.__setattr__arbeiten, aber ich fand ihn trotzdem nützlich:

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)

__setitem__Je nach Anwendungsfall müssen Sie möglicherweise mehr Dinge (wie ) überschreiben .

Dangirsh
quelle
Ich habe mir etwas Ähnliches ausgedacht, bevor ich das gesehen habe, aber es wurde verwendet getattr, um einen Standardwert für bereitzustellen frozen. Das hat die Dinge ein bisschen vereinfacht. stackoverflow.com/a/22545808/5987
Mark Ransom
Ich mag diesen Ansatz am besten, aber Sie brauchen den __new__Override nicht. Innen __setattr__ersetzen Sie einfach die Bedingung durchif name != '_frozen' and getattr(self, "_frozen", False)
Pete Cacioppi
Es ist auch nicht erforderlich, die Klasse beim Bau einzufrieren. Sie können es jederzeit einfrieren, wenn Sie eine freeze()Funktion bereitstellen . Das Objekt wird dann "einmal einfrieren". Schließlich object.__setattr__ist es albern , sich Sorgen zu machen , denn "wir sind alle Erwachsene hier".
Pete Cacioppi
2

Ab Python 3.7 können Sie den @dataclassDekorator in Ihrer Klasse verwenden und er ist unveränderlich wie eine Struktur! Es kann jedoch eine __hash__()Methode zu Ihrer Klasse hinzufügen oder nicht . Zitat:

hash () wird von integriertem hash () verwendet und wenn Objekte zu Hash-Sammlungen wie Wörterbüchern und Mengen hinzugefügt werden. Ein Hash () bedeutet, dass Instanzen der Klasse unveränderlich sind. Die Veränderbarkeit ist eine komplizierte Eigenschaft, die von der Absicht des Programmierers, der Existenz und dem Verhalten von eq () sowie den Werten der eq- und Frozen-Flags im dataclass () -Dekorator abhängt.

Standardmäßig fügt dataclass () implizit keine hash () -Methode hinzu, es sei denn, dies ist sicher. Es wird auch keine vorhandene explizit definierte hash () -Methode hinzugefügt oder geändert . Das Festlegen des Klassenattributs hash = None hat für Python eine bestimmte Bedeutung, wie in der Dokumentation zu hash () beschrieben.

Wenn hash () nicht explizit definiert ist oder auf None festgelegt ist, kann dataclass () eine implizite hash () -Methode hinzufügen . Obwohl nicht empfohlen, können Sie dataclass () zwingen, eine hash () -Methode mit unsafe_hash = True zu erstellen . Dies kann der Fall sein, wenn Ihre Klasse logisch unveränderlich ist, aber dennoch mutiert werden kann. Dies ist ein spezieller Anwendungsfall und sollte sorgfältig abgewogen werden.

Hier das Beispiel aus den oben verlinkten Dokumenten:

@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
Gemeinschaft
quelle
1
Sie müssen verwenden frozen, dh @dataclass(frozen=True), aber es blockiert im Grunde die Verwendung von __setattr__und __delattr__wie in den meisten anderen Antworten hier. Dies geschieht nur auf eine Weise, die mit den anderen Optionen von Datenklassen kompatibel ist.
CS
2

Sie können setattr überschreiben und trotzdem init verwenden , um die Variable festzulegen . Sie würden Super Class Setattr verwenden . Hier ist der Code.

Klasse unveränderlich:
    __slots__ = ('a', 'b')
    def __init __ (self, a, b):
        super () .__ setattr __ ('a', a)
        super () .__ setattr __ ('b', b)

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

    def __setattr __ (self, * ignoriert):
        NotImplementedError auslösen

    def __delattr __ (self, * ignoriert):
        NotImplementedError auslösen
Shameer Ali
quelle
Oder einfach passstattraise NotImplementedError
jonathan.scholbach
In diesem Fall ist es überhaupt keine gute Idee, in __setattr__ und __delattr__ "pass" zu machen. Der einfache Grund ist, dass jemand, der einem Feld / einer Eigenschaft einen Wert zuweist, natürlich erwartet, dass das Feld geändert wird. Wenn Sie dem Pfad der "geringsten Überraschung" folgen möchten (wie Sie sollten), müssen Sie einen Fehler auslösen. Ich bin mir jedoch nicht sicher, ob NotImplementedError der richtige ist. Ich würde so etwas wie "Feld / Eigentum ist unveränderlich" ansprechen. Fehler ... Ich denke, eine benutzerdefinierte Ausnahme sollte ausgelöst werden.
Liebling
1

Das Drittanbieter- attrModul bietet diese Funktionalität .

Edit: python 3.7 hat diese idee mit in die stdlib übernommen @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

attrImplementiert eingefrorene Klassen durch Überschreiben __setattr__und hat laut Dokumentation zu jedem Zeitpunkt eine geringfügige Auswirkung auf die Leistung.

Wenn Sie die Gewohnheit haben, Klassen als Datentypen zu verwenden, attrkann dies besonders nützlich sein, da es sich um das Boilerplate für Sie kümmert (aber keine Magie ausführt). Insbesondere werden neun dunder (__X__) Methoden für Sie geschrieben (es sei denn, Sie deaktivieren eine davon), einschließlich repr, init, hash und aller Vergleichsfunktionen.

attrbietet auch einen Helfer für__slots__ .

cmc
quelle
1

Also schreibe ich jeweils von Python 3:

I) mit Hilfe des Datenklassendekorators und setze eingefroren = True. Wir können unveränderliche Objekte in Python erstellen.

Dazu muss die Datenklasse aus der Datenklasse lib importiert und gefroren = True gesetzt werden

Ex.

aus Datenklassen Datenklasse importieren

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

o / p:

l = Standort ("Delhi", 112.345, 234.788) l.name 'Delhi' l.Longitude 112.345 l.latitude 234.788 l.name = "Kolkata" -Datenklassen.FrozenInstanceError: Feld 'Name' kann nicht zugewiesen werden

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

RaghuGolla
quelle
0

Ein alternativer Ansatz besteht darin, einen Wrapper zu erstellen, der eine Instanz unveränderlich macht.

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)

Dies ist in Situationen nützlich, in denen nur einige Instanzen unveränderlich sein müssen (wie Standardargumente von Funktionsaufrufen).

Kann auch in unveränderlichen Fabriken eingesetzt werden wie:

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

Schützt auch vor object.__setattr__anderen Tricks, ist aber aufgrund der dynamischen Natur von Python fehlbar.

Mark Horvath
quelle
0

Ich habe die gleiche Idee wie Alex verwendet: eine Metaklasse und einen "Init-Marker", aber in Kombination mit dem Überschreiben von __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)

Hinweis: Ich rufe die Meta-Klasse direkt auf, damit sie sowohl für Python 2.x als auch für 3.x funktioniert.

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

Es funktioniert auch mit 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.

... und Mehrfachvererbung:

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

Beachten Sie jedoch, dass veränderbare Attribute unveränderlich bleiben müssen:

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

Eine Sache, die hier nicht wirklich enthalten ist, ist die völlige Unveränderlichkeit ... nicht nur das übergeordnete Objekt, sondern auch alle Kinder. Tupel / Frozensets können beispielsweise unveränderlich sein, die Objekte, zu denen sie gehören, jedoch möglicherweise nicht. Hier ist eine kleine (unvollständige) Version, die die Unveränderlichkeit bis zum Ende anständig erzwingt:

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

Sie können setAttr einfach in der endgültigen Anweisung von init überschreiben. Dann können Sie konstruieren, aber nicht ändern. Natürlich können Sie das usint-Objekt immer noch überschreiben. setAttr aber in der Praxis haben die meisten Sprachen irgendeine Form der Reflexion, so dass Unveränderlichkeit immer eine undichte Abstraktion ist. Bei der Unveränderlichkeit geht es eher darum, zu verhindern, dass Kunden versehentlich gegen den Vertrag eines Objekts verstoßen. Ich benutze:

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

Die ursprünglich angebotene Lösung war falsch. Diese wurde basierend auf den Kommentaren unter Verwendung der Lösung von hier aktualisiert

Die ursprüngliche Lösung ist auf interessante Weise falsch, daher ist sie unten enthalten.

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

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

Ausgabe :

1
2
Attempted To Modify Immutable Object
1
2

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

Ursprüngliche Implementierung:

In den Kommentaren wurde zu Recht darauf hingewiesen, dass dies tatsächlich nicht funktioniert, da es die Erstellung von mehr als einem Objekt verhindert, wenn Sie die class setattr-Methode überschreiben, was bedeutet, dass eine Sekunde nicht als self.a = will erstellt werden kann Fehler bei der zweiten Initialisierung.

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
quelle
1
Das wird nicht funktionieren: Sie überschreiben die Methode für die Klasse , sodass Sie NotImplementedError erhalten, sobald Sie versuchen, eine zweite Instanz zu erstellen.
Slinkp
1
Wenn Sie diesen Ansatz verfolgen möchten, beachten Sie, dass es schwierig ist, spezielle Methoden zur Laufzeit zu überschreiben: Weitere Problemumgehungen finden Sie unter stackoverflow.com/a/16426447/137635 .
Slinkp
0

Die folgende Grundlösung befasst sich mit dem folgenden Szenario:

  • __init__() kann wie gewohnt auf die Attribute zugegriffen werden.
  • Nachdem das Objekt nur für Attributänderungen eingefroren wurde :

Die Idee ist, die __setattr__Methode zu überschreiben und ihre Implementierung jedes Mal zu ersetzen, wenn der Status des eingefrorenen Objekts geändert wird.

Wir brauchen also eine Methode (_freeze ), die diese beiden Implementierungen speichert und auf Anfrage zwischen ihnen wechselt.

Dieser Mechanismus kann innerhalb der Benutzerklasse implementiert oder von einer speziellen FreezerKlasse geerbt werden, wie unten gezeigt:

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

Genau wie ein dict

Ich habe eine Open-Source-Bibliothek, in der ich Dinge auf funktionale Weise erledige , sodass das Verschieben von Daten in einem unveränderlichen Objekt hilfreich ist. Ich möchte jedoch nicht mein Datenobjekt transformieren müssen, damit der Client mit ihnen interagieren kann. Also habe ich mir das ausgedacht - es gibt dir ein diktartiges Objekt, das unveränderlich ist + einige Hilfsmethoden .

Dank an Sven Marnach in seiner Antwort für die grundlegende Implementierung der Einschränkung der Aktualisierung und Löschung von Immobilien.

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__

Hilfsmethoden

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)

Beispiele

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
quelle