Jak zrobić niezmienny obiekt w Pythonie?

181

Chociaż nigdy tego nie potrzebowałem, uderzyło mnie tylko, że utworzenie niezmiennego obiektu w Pythonie może być nieco trudne. Nie możesz po prostu nadpisać __setattr__, ponieważ wtedy nie możesz nawet ustawić atrybutów w __init__. Podklasowanie krotki to sztuczka, która działa:

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

Ale wtedy masz dostęp do wszystkiego ai bzmiennych przez self[0]i self[1], co jest denerwujące.

Czy jest to możliwe w Pure Python? Jeśli nie, jak mam to zrobić z rozszerzeniem C?

(Odpowiedzi, które działają tylko w Pythonie 3 są dopuszczalne).

Aktualizacja:

Więc instacji krotka jest sposobem, aby to zrobić w Pure Python, który działa dobrze z wyjątkiem dodatkowej możliwości uzyskania dostępu do danych przez [0], [1]itd Tak, aby zakończyć to pytanie wszystkim, co jest brakuje howto to zrobić „prawidłowo”, co w języku C Podejrzewam, że byłoby to całkiem proste, po prostu nie wdrażając żadnego geititemlub setattribute, itp. Ale zamiast zrobić to sam, oferuję za to nagrodę, ponieważ jestem leniwy. :)

Lennart Regebro
źródło
2
Czy Twój kod nie ułatwia dostępu do atrybutów za pośrednictwem .ai .b? W końcu wydaje się, że po to istnieją właściwości.
Sven Marnach
1
@Sven Marnach: Tak, ale [0] i [1] nadal działają i dlaczego mieliby to robić? Nie chcę ich. :) Może idea niezmiennego obiektu z atrybutami jest bezsensowna? :-)
Lennart Regebro
2
Jeszcze jedna uwaga: NotImplementedma służyć tylko jako wartość zwracana w przypadku bogatych porównań. Wartość zwracana dla __setatt__()jest i tak raczej bezcelowa, ponieważ zazwyczaj w ogóle jej nie widać. Kod jak immutable.x = 42po cichu nic nie robi. Zamiast tego powinieneś podbić TypeError.
Sven Marnach
1
@Sven Marnach: OK, byłem zaskoczony, ponieważ myślałem, że w tej sytuacji możesz podnieść NotImplemented, ale to daje dziwny błąd. Więc zamiast tego zwróciłem go i wydawało się, że działa. TypeError nabrał oczywistego sensu, gdy zobaczyłem, że go używasz.
Lennart Regebro
1
@Lennart: Możesz podbić NotImplementedError, ale TypeErrorto właśnie podnosi krotka, jeśli spróbujesz ją zmodyfikować.
Sven Marnach

Odpowiedzi:

115

Jeszcze jedno rozwiązanie, o którym właśnie pomyślałem: Najprostszy sposób na uzyskanie takiego samego zachowania, jak w przypadku oryginalnego kodu

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

Nie rozwiązuje problemu polegającego na tym, że atrybuty można uzyskać za pośrednictwem [0]itp., Ale przynajmniej jest znacznie krótszy i zapewnia dodatkową korzyść w postaci zgodności z picklei copy.

namedtupletworzy typ podobny do tego, który opisałem w tej odpowiedzi , tj. wyprowadzony z tuplei wykorzystujący __slots__. Jest dostępny w Pythonie 2.6 lub nowszym.

Sven Marnach
źródło
7
Zaletą tego wariantu w porównaniu z ręcznie napisanym analogiem (nawet w Pythonie 2.5 (użycie verboseparametru do namedtuplekodu jest łatwe do wygenerowania)) jest pojedynczy interfejs / implementacja a namedtuplejest lepsza niż dziesiątki nieco innych ręcznie napisanych interfejsów / implementacji, które zrób prawie to samo.
jfs
2
OK, otrzymujesz „najlepszą odpowiedź”, ponieważ to najłatwiejszy sposób. Sebastian dostaje nagrodę za wykonanie krótkiej implementacji Cythona. Twoje zdrowie!
Lennart Regebro
1
Inną cechą niezmiennych obiektów jest to, że gdy przekazujesz je jako parametr przez funkcję, są one kopiowane przez wartość, a nie tworzone jest inne odwołanie. Czy namedtuples byłby kopiowany przez wartość podczas przekazywania przez funkcje?
hlin117
4
@ hlin117: Każdy parametr jest przekazywany jako odniesienie do obiektu w Pythonie, niezależnie od tego, czy jest zmienny, czy niezmienny. W przypadku niezmiennych obiektów tworzenie kopii byłoby szczególnie bezcelowe - ponieważ i tak nie możesz zmienić obiektu, równie dobrze możesz przekazać odniesienie do oryginalnego obiektu.
Sven Marnach
Czy możesz użyć namedtuple wewnętrznie wewnątrz klasy zamiast tworzenia wystąpienia obiektu zewnętrznie? Jestem bardzo nowy w Pythonie, ale zaletą Twojej drugiej odpowiedzi jest to, że mogę mieć klasę, która ukrywa szczegóły, a także ma moc takich rzeczy, jak parametry opcjonalne. Jeśli spojrzę tylko na tę odpowiedź, wydaje mi się, że muszę mieć wszystko, co używa mojej klasy instancji nazwanych krotek. Dziękuję za obie odpowiedzi.
Asaf
78

Najłatwiej to zrobić, używając __slots__:

class A(object):
    __slots__ = []

Instancje Asą teraz niezmienne, ponieważ nie możesz ustawić dla nich żadnych atrybutów.

Jeśli chcesz, aby instancje klasy zawierały dane, możesz to połączyć z wyprowadzaniem z 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

Edycja : jeśli chcesz pozbyć się indeksowania, możesz zastąpić __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

Zauważ, że nie możesz użyć operator.itemgetterdla właściwości w tym przypadku, ponieważ będzie to polegać Point.__getitem__()zamiast tuple.__getitem__(). Fuerthermore to nie przeszkodzi w użyciu tuple.__getitem__(p, 0), ale nie mogę sobie wyobrazić, jak powinno to stanowić problem.

Nie sądzę, aby „właściwym” sposobem tworzenia niezmiennego obiektu było pisanie rozszerzenia C. Python zwykle polega na tym, że osoby wdrażające biblioteki i użytkownicy biblioteki wyrażają zgodę na to osoby dorosłe i zamiast naprawdę wymuszać interfejs, interfejs powinien być jasno określony w dokumentacji. Dlatego nie rozważam możliwości obejścia problemu, który został pominięty, __setattr__()poprzez wywołanie object.__setattr__()problemu. Jeśli ktoś to robi, robi to na własne ryzyko.

Sven Marnach
źródło
1
Czy nie byłoby lepszym pomysłem użycie tupletutaj __slots__ = ()zamiast __slots__ = []? (Tylko wyjaśnienie)
user225312
1
@sukhbir: Myślę, że to w ogóle nie ma znaczenia. Dlaczego wolisz krotkę?
Sven Marnach
1
@Sven: Zgadzam się, że to nie ma znaczenia (z wyjątkiem części związanej z prędkością, którą możemy zignorować), ale pomyślałem o tym w ten sposób: __slots__nie zostanie to zmienione, prawda? Ma to na celu jednorazowe zidentyfikowanie atrybutów, które można ustawić. Czy w takim przypadku nie tuplewydaje się to raczej naturalnym wyborem?
user225312
5
Ale przy pustym polu __slots__nie mogę ustawić żadnych atrybutów. A jeśli mam, __slots__ = ('a', 'b')to atrybuty a i b są nadal zmienne.
Lennart Regebro
Ale twoje rozwiązanie jest lepsze niż zastąpienie, __setattr__więc jest to ulepszenie w stosunku do mojego. +1 :)
Lennart Regebro
50

..jak to zrobić „poprawnie” w C ..

Możesz użyć Cythona do stworzenia typu rozszerzenia dla Pythona:

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

Działa zarówno w Pythonie 2.x, jak i 3.

Testy

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

Jeśli nie masz nic przeciwko obsłudze indeksowania collections.namedtuple, preferowane jest sugerowane przez @Sven Marnach :

Immutable = collections.namedtuple("Immutable", "a b")
jfs
źródło
@Lennart: Instancje namedtuple(a dokładniej typu zwracanego przez funkcję namedtuple()) są niezmienne. Zdecydowanie.
Sven Marnach
@Lennart Regebro: namedtupleprzechodzi wszystkie testy (z wyjątkiem obsługi indeksowania). Jaki wymóg przegapiłem?
jfs
Tak, masz rację, utworzyłem nazwany typtuple, utworzyłem go, a następnie wykonałem test na typie zamiast instancji. Heh. :-)
Lennart Regebro
czy mogę zapytać, po co tu potrzebne są słabe odniesienia?
McSinyx
1
@McSinyx: w przeciwnym razie obiekty nie mogą być używane w kolekcjach slabref. Co dokładnie jest __weakref__w Pythonie?
jfs
40

Innym pomysłem byłoby całkowite zablokowanie __setattr__i użycie object.__setattr__w konstruktorze:

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

Oczywiście możesz użyć object.__setattr__(p, "x", 3)do zmodyfikowania Pointinstancji p, ale Twoja oryginalna implementacja ma ten sam problem (spróbuj tuple.__setattr__(i, "x", 42)na Immutableinstancji).

Możesz zastosować tę samą sztuczkę w swojej oryginalnej implementacji: pozbyć się __getitem__()i wykorzystać tuple.__getitem__()w swoich funkcjach właściwości.

Sven Marnach
źródło
11
Nie przejmowałbym się, że ktoś celowo modyfikuje obiekt za pomocą nadklasy ” __setattr__, bo nie chodzi o to, aby być niezawodnym. Chodzi o to, aby było jasne, że nie należy go modyfikować i aby zapobiec modyfikacjom przez pomyłkę.
zvone
18

Możesz stworzyć @immutabledekorator, który zastąpi __setattr__ i zmieni listę __slots__na pustą, a następnie ozdobi nią __init__metodę.

Edycja: jak zauważył OP, zmiana __slots__atrybutu zapobiega tylko tworzeniu nowych atrybutów , a nie modyfikacji.

Edit2: Oto implementacja:

Edycja3: Użycie __slots__psuje ten kod, ponieważ jeśli zatrzymuje tworzenie pliku __dict__. Szukam alternatywy.

Edit4: Cóż, to wszystko. To trochę hakerskie, ale działa jak ćwiczenie :-)

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
źródło
1
Utworzenie (klasy?) Dekoratora lub metaklasy z rozwiązania jest rzeczywiście dobrym pomysłem, ale pytanie brzmi, jakie jest rozwiązanie. :)
Lennart Regebro
3
object.__setattr__()łamie to stackoverflow.com/questions/4828080/…
jfs
W rzeczy samej. Po prostu kontynuowałem ćwiczenie na dekoratorach.
PaoloVictor
13

Korzystanie z zamrożonej klasy danych

W przypadku Pythona 3.7+ możesz użyć klasy danych z frozen=Trueopcją , która jest bardzo Pythonowym i łatwym w utrzymaniu sposobem robienia tego, co chcesz.

Wyglądałoby to mniej więcej tak:

from dataclasses import dataclass

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

Ponieważ podpowiedzi typu są wymagane dla pól klas danych, użyłem Any z typingmodułu .

Powody, dla których NIE należy używać nazwy Namedtuple

Przed Pythonem 3.7 często zdarzało się, że namedtuples były używane jako niezmienne obiekty. Może to być trudne na wiele sposobów, jednym z nich jest to, że __eq__metoda między nazwanymi krotkami nie uwzględnia klas obiektów. Na przykład:

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

Jak widać, nawet jeśli typy obj1i obj2są różne, nawet jeśli nazwy ich pól są różne, obj1 == obj2nadal daje True. Dzieje się tak, ponieważ __eq__użyta metoda jest metodą krotki, która porównuje tylko wartości pól, biorąc pod uwagę ich pozycje. Może to być ogromne źródło błędów, szczególnie jeśli tworzysz podklasy tych klas.

Jundiaius
źródło
10

Myślę, że nie jest to całkowicie możliwe, z wyjątkiem użycia krotki lub nazwanej krotki. Bez względu na wszystko, jeśli zastąpisz __setattr__()użytkownika, zawsze możesz go ominąć, dzwoniąc object.__setattr__()bezpośrednio. __setattr__Gwarantujemy, że każde rozwiązanie, od którego zależy, nie zadziała.

Poniżej znajduje się przybliżenie najbliższego, jakie możesz uzyskać bez użycia jakiejś krotki:

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__

ale zepsuje się, jeśli spróbujesz wystarczająco mocno:

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

ale użycie przez Svena namedtuplejest naprawdę niezmienne.

Aktualizacja

Ponieważ pytanie zostało zaktualizowane, aby zapytać, jak zrobić to poprawnie w C, oto moja odpowiedź, jak zrobić to poprawnie w Cythonie:

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

i a, setup.pyaby go skompilować (używając polecenia 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
)

Następnie, aby to wypróbować:

>>> 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
źródło
Dzięki za kod Cython, Cython jest niesamowity. Implementacja JF Sebastiana z readonly jest jednak schludniejsza i dotarła jako pierwsza, więc dostaje nagrodę.
Lennart Regebro
5

Utworzyłem niezmienne klasy, zastępując __setattr__i zezwalając na zestaw, jeśli wywołującym jest __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)

To jeszcze nie wystarczy, ponieważ pozwala każdemu ___init__zmienić obiekt, ale masz pomysł.

Ned Batchelder
źródło
object.__setattr__()łamie to stackoverflow.com/questions/4828080/ ...
jfs
3
Korzystanie z inspekcji stosu, aby upewnić się, że wywołujący __init__nie jest zbyt satysfakcjonujący.
gb.
5

Oprócz doskonałych innych odpowiedzi lubię dodać metodę dla Pythona 3.4 (a może 3.3). Ta odpowiedź opiera się na kilku wcześniejszych odpowiedziach na to pytanie.

W Pythonie 3.4 można używać właściwości bez funkcji ustawiających do tworzenia elementów klas, których nie można modyfikować. (We wcześniejszych wersjach możliwe było przypisywanie do właściwości bez metody ustawiającej).

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

Możesz go używać w ten sposób:

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

który będzie drukowany "constant"

Ale wezwanie instance.a=10spowoduje:

AttributeError: can't set attribute

Wyjaśnienie: właściwości bez seterów są bardzo nową funkcją Pythona 3.4 (i myślę, że 3.3). Jeśli spróbujesz przypisać do takiej właściwości, zostanie zgłoszony błąd. Używając gniazd, ograniczam zmienne członkowskie do __A_a(czyli jest __a).

Problem: Przypisywanie do _A__ajest nadal możliwe ( instance._A__a=2). Ale jeśli przypiszesz do zmiennej prywatnej, to twoja wina ...

Ta odpowiedź między innymi jednak zniechęca do korzystania z __slots__. Preferowane może być użycie innych sposobów zapobiegania tworzeniu atrybutów.

Bernhard
źródło
propertyjest również dostępny w Pythonie 2 (spójrz na kod w samym pytaniu). Nie tworzy niezmiennego obiektu, spróbuj testów z mojej odpowiedzi np. instance.b = 1Tworzy nowy batrybut.
jfs
Racja, tak naprawdę pytanie brzmi, jak temu zapobiec, A().b = "foo"tj. Nie pozwolić na ustawienie nowych atrybutów.
Lennart Regebro
Propertis bez metody ustawiającej generuje błąd w Pythonie 3.4, jeśli spróbujesz przypisać tę właściwość. We wcześniejszych wersjach ustawiacz był generowany niejawnie.
Bernhard
@Lennart: Moje rozwiązanie jest odpowiedzią na podzbiór przypadków użycia dla niezmiennych obiektów i uzupełnieniem poprzednich odpowiedzi. Jednym z powodów, dla których mógłbym chcieć niezmiennego obiektu, jest to, że mogę go skasować, w takim przypadku moje rozwiązanie może działać. Ale masz rację, to nie jest niezmienny przedmiot.
Bernhard
@ jf-sebastian: Zmieniłem moją odpowiedź, aby używać gniazd do zapobiegania tworzeniu atrybutów. Nowością w mojej odpowiedzi w porównaniu z innymi odpowiedziami jest to, że używam właściwości pythona 3.4, aby uniknąć zmiany istniejących atrybutów. Chociaż to samo osiągnięto we wcześniejszych odpowiedziach, mój kod jest krótszy z powodu zmiany zachowania właściwości.
Bernhard
5

Oto eleganckie rozwiązanie:

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

Dziedzicz z tej klasy, zainicjuj swoje pola w konstruktorze i gotowe.

Aleksandra Ryżowa
źródło
1
ale z tą logiką można przypisać nowe atrybuty do obiektu
javed
3

Jeśli interesują Cię obiekty zachowujące się, to namedtuple jest prawie Twoim rozwiązaniem.

Jak opisano na końcu dokumentacji namedtuple , możesz wyprowadzić własną klasę z namedtuple; a następnie możesz dodać żądane zachowanie.

Na przykład (kod pobrany bezpośrednio z dokumentacji ):

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)

Spowoduje to:

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

To podejście działa zarówno dla Pythona 3, jak i Pythona 2.7 (przetestowane również na IronPythonie).
Jedynym minusem jest to, że drzewo dziedziczenia jest trochę dziwne; ale to nie jest coś, czym zwykle się bawisz.

obrabować
źródło
1
Python 3.6+ obsługuje to bezpośrednio, używającclass Point(typing.NamedTuple):
Elazar
3

Klasy dziedziczące z następującej Immutableklasy są niezmienne, podobnie jak ich wystąpienia, po __init__zakończeniu wykonywania ich metody. Ponieważ jest to czysty Python, jak zauważyli inni, nic nie stoi na przeszkodzie, aby ktoś użył mutujących specjalnych metod z bazy objecti type, ale to wystarczy, aby uniemożliwić każdemu przypadkową mutację klasy / instancji.

Działa poprzez przejmowanie procesu tworzenia klas za pomocą metaklasy.

"""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
źródło
2

Potrzebowałem tego jakiś czas temu i postanowiłem stworzyć dla niego pakiet w Pythonie. Pierwsza wersja jest teraz na PyPI:

$ pip install immutable

Używać:

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

Pełna dokumentacja tutaj: https://github.com/theengineear/immutable

Mam nadzieję, że to pomoże, otacza nazwane trzy elementy, jak już omówiono, ale znacznie upraszcza tworzenie instancji.

silnik
źródło
2

Ten sposób nie przestaje object.__setattr__działać, ale nadal uważam go za przydatny:

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)

może być konieczne zastąpienie większej liczby rzeczy (takich jak __setitem__) w zależności od przypadku użycia.

dangirsh
źródło
Wymyśliłem coś podobnego, zanim to zobaczyłem, ale użyłem go, getattraby móc podać domyślną wartość frozen. To trochę uprościło sprawę. stackoverflow.com/a/22545808/5987
Mark Ransom
Najbardziej podoba mi się to podejście, ale nie potrzebujesz zmiany __new__. Wewnątrz __setattr__wystarczy zamienić warunek naif name != '_frozen' and getattr(self, "_frozen", False)
Pete Cacioppi
Ponadto nie ma potrzeby zamrażania klasy podczas budowy. Możesz go zatrzymać w dowolnym momencie, jeśli podasz freeze()funkcję. Obiekt zostanie wówczas „raz zamrożony”. Wreszcie zamartwianie się object.__setattr__jest głupie, ponieważ „wszyscy jesteśmy tutaj dorośli”.
Pete Cacioppi
2

Począwszy od Python 3.7, możesz używać @dataclass dekoratora w swojej klasie i będzie on niezmienny jak struktura! Chociaż może, ale nie __hash__()musi, dodać metodę do twojej klasy. Zacytować:

Funkcja hash () jest używana przez wbudowaną funkcję hash () oraz gdy obiekty są dodawane do haszowanych kolekcji, takich jak słowniki i zestawy. Posiadanie hash () oznacza, że instancje klasy są niezmienne. Mutowalność to skomplikowana właściwość zależna od intencji programisty, istnienia i zachowania eq () oraz wartości eq i zamrożonych flag w dekoratorze dataclass ().

Domyślnie dataclass () nie doda niejawnie metody hash (), chyba że jest to bezpieczne. Ani nie doda ani nie zmieni istniejącego jawnie zdefiniowanego skrótu metody (). Ustawienie atrybutu class hash = None ma określone znaczenie dla języka Python, zgodnie z opisem w dokumentacji hash ().

Jeśli hash () nie jest jawnie zdefiniowany lub jest ustawiony na None, dataclass () może dodać niejawne metodę hash (). Chociaż nie jest to zalecane, możesz wymusić na dataclass () utworzenie metody hash () z unsafe_hash = True. Może tak być w przypadku, gdy twoja klasa jest logicznie niezmienna, ale mimo to może zostać zmutowana. Jest to specjalny przypadek użycia i należy go dokładnie rozważyć.

Oto przykład z dokumentów, do których link powyżej:

@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
Społeczność
źródło
1
musisz użyć frozen, tj. @dataclass(frozen=True), ale zasadniczo blokuje użycie __setattr__i __delattr__jak w większości innych odpowiedzi tutaj. Robi to po prostu w sposób zgodny z innymi opcjami klas danych.
CS
2

Możesz nadpisać setattr i nadal używać init do ustawiania zmiennej. Użyłbyś super klasy setattr . tutaj jest kod.

klasa Niezmienna:
    __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):
        podnieść NotImplementedError

    def __delattr __ (self, * ignored):
        podnieść NotImplementedError
Shameer Ali
źródło
Albo po prostu passzamiastraise NotImplementedError
jonathan.scholbach
W tym przypadku nie jest dobrym pomysłem wykonywanie funkcji „pass” w __setattr__ i __delattr__. Prosty powód jest taki, że jeśli ktoś przypisze wartość do pola / właściwości, to naturalnie oczekuje, że pole zostanie zmienione. Jeśli chcesz podążać ścieżką „najmniejszego zaskoczenia” (tak jak powinieneś), to musisz wywołać błąd. Ale nie jestem pewien, czy NotImplementedError jest właściwym rozwiązaniem. Podniósłbym coś w stylu „Pole / własność jest niezmienna”. błąd ... Myślę, że powinien zostać wyrzucony niestandardowy wyjątek.
kochanie
1

Tę funkcjonalnośćattr zapewnia moduł strony trzeciej .

Edycja: python 3.7 zaadoptował ten pomysł do standardowej biblioteki z @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

attrimplementuje zamrożone klasy, zastępując je __setattr__i ma niewielki wpływ na wydajność w każdym czasie tworzenia wystąpienia, zgodnie z dokumentacją.

Jeśli masz zwyczaj używania klas jako typów danych, attrmoże to być szczególnie przydatne, ponieważ zajmuje się za Ciebie szablonem (ale nie robi żadnej magii). W szczególności zapisuje dla Ciebie dziewięć metod dunder (__X__) (chyba że wyłączysz którąkolwiek z nich), w tym repr, init, hash i wszystkie funkcje porównawcze.

attrzapewnia również pomocnika dla__slots__ .

cmc
źródło
1

Tak więc piszę odpowiednio dla Pythona 3:

I) za pomocą dekoratora klasy danych i ustaw frozen = True. możemy tworzyć niezmienne obiekty w Pythonie.

w tym celu należy zaimportować klasę danych z klas danych lib i ustawić frozen = True

dawny.

z klas danych importuj klasę danych

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

o / p:

l = Lokalizacja ("Delhi", 112.345, 234.788) l.name 'Delhi' l.longitude 112.345 l.latitude 234.788 l.name = "Kolkata" dataclasses.FrozenInstanceError: nie można przypisać do pola 'name'

Źródło: https://realpython.com/python-data-classes/

RaghuGolla
źródło
0

Alternatywnym podejściem jest utworzenie opakowania, które sprawia, że ​​instancja jest niezmienna.

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)

Jest to przydatne w sytuacjach, w których tylko niektóre wystąpienia muszą być niezmienne (np. Domyślne argumenty wywołań funkcji).

Może być również używany w niezmiennych fabrykach, takich jak:

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

Chroni również przed object.__setattr__innymi sztuczkami, ale nie radzi sobie z nimi ze względu na dynamiczną naturę Pythona.

Mark Horvath
źródło
0

Użyłem tego samego pomysłu co Alex: meta-klasa i „znacznik inicjalizacji”, ale w połączeniu z nadpisywaniem __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)

Uwaga: wywołuję meta-klasę bezpośrednio, aby działała zarówno w Pythonie 2.x, jak i 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.

Działa również ze slotami ...:

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

... i wielokrotne dziedziczenie:

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

Należy jednak pamiętać, że zmienne atrybuty pozostają zmienne:

>>> t3 = T3(12, [4, 7], 'a', 'b')
>>> t3.y.append(5)
>>> t3.y
[4, 7, 5]
Michael Amrhein
źródło
0

Jedną rzeczą, która tak naprawdę nie jest tutaj uwzględniona, jest całkowita niezmienność ... nie tylko obiektu nadrzędnego, ale także wszystkich dzieci. krotki / zestawy zamrożone mogą być na przykład niezmienne, ale obiekty, których są częścią, mogą nie być. Oto mała (niekompletna) wersja, która porządnie wymusza niezmienność przez cały czas:

# 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
źródło
0

Możesz po prostu nadpisać setAttr w końcowej instrukcji init. WTEDY możesz konstruować, ale nie zmieniać. Oczywiście nadal można nadpisać obiekt usint. setAttr ale w praktyce większość języków ma jakąś formę refleksji, więc niezmienność jest zawsze nieszczelną abstrakcją. Niezmienność polega bardziej na zapobieganiu przypadkowemu naruszeniu przez klientów umowy obiektu. Używam:

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

Oferowane oryginalne rozwiązanie było nieprawidłowe, zostało to zaktualizowane na podstawie komentarzy korzystających z rozwiązania z tego miejsca

Oryginalne rozwiązanie jest w ciekawy sposób błędne, więc jest zawarte na dole.

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

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

Wynik :

1
2
Attempted To Modify Immutable Object
1
2

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

Oryginalna implementacja:

W komentarzach wskazano poprawnie, że to w rzeczywistości nie działa, ponieważ zapobiega tworzeniu więcej niż jednego obiektu, ponieważ nadpisujesz metodę setattr klasy, co oznacza, że ​​drugi nie może zostać utworzony jako self. A = will niepowodzenie przy drugiej inicjalizacji.

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
źródło
1
To nie zadziała: nadpisujesz metodę w klasie , więc otrzymasz NotImplementedError, gdy tylko spróbujesz utworzyć drugą instancję.
slinkp
1
Jeśli chcesz zastosować to podejście, pamiętaj, że trudno jest przesłonić specjalne metody w czasie wykonywania: zobacz stackoverflow.com/a/16426447/137635, aby uzyskać kilka obejść tego problemu.
slinkp
0

Poniższe podstawowe rozwiązanie dotyczy następującego scenariusza:

  • __init__() można zapisać, uzyskując dostęp do atrybutów w zwykły sposób.
  • PO zamrożeniu OBIEKTU tylko dla zmian atrybutów :

Chodzi o to, aby przesłonić __setattr__metodę i zastąpić jej implementację za każdym razem, gdy zmienia się stan zamrożenia obiektu.

Potrzebujemy więc metody ( _freeze), która przechowuje te dwie implementacje i przełącza się między nimi na żądanie.

Ten mechanizm może być zaimplementowany w klasie użytkownika lub dziedziczony ze specjalnej Freezerklasy, jak pokazano poniżej:

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
źródło
0

Prawdziwy dict

Mam bibliotekę open source, w której robię rzeczy w funkcjonalny sposób, więc przenoszenie danych w niezmiennym obiekcie jest pomocne. Jednak nie chcę przekształcać obiektu danych, aby klient mógł z nimi współdziałać. Tak więc wymyśliłem to - daje ci dykt, taki jak obiekt, który jest niezmienny + kilka metod pomocniczych.

Podziękowania dla Svena Marnacha w jego odpowiedzi za podstawową implementację ograniczania aktualizacji i usuwania nieruchomości.

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__

Metody pomocnicze

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)

Przykłady

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
źródło