Zapobiegaj tworzeniu nowych atrybutów poza __init__

82

Chcę mieć możliwość stworzenia klasy (w Pythonie), która po zainicjowaniu __init__nie akceptuje nowych atrybutów, ale akceptuje modyfikacje istniejących atrybutów. Jest kilka hakerskich sposobów, aby to zrobić, na przykład mając __setattr__metodę taką jak

def __setattr__(self, attribute, value):
    if not attribute in self.__dict__:
        print "Cannot set %s" % attribute
    else:
        self.__dict__[attribute] = value

a potem edycję __dict__bezpośrednio w środku __init__, ale zastanawiałem się, czy istnieje „właściwy” sposób na zrobienie tego?

astrofrog
źródło
1
katrielalex ma dobre strony. Nie ma w tym nic dziwnego. Możesz uniknąć używania, __setattr__ale to prawdopodobnie byłoby hakerskie.
aaronasterling
Nie rozumiem, dlaczego to jest hacky? To najlepsze rozwiązanie, jakie mogłem wymyślić i dużo bardziej zwięzłe niż niektóre inne proponowane.
Chris B

Odpowiedzi:

81

Nie użyłbym __dict__bezpośrednio, ale możesz dodać funkcję, aby jawnie „zamrozić” instancję:

class FrozenClass(object):
    __isfrozen = False
    def __setattr__(self, key, value):
        if self.__isfrozen and not hasattr(self, key):
            raise TypeError( "%r is a frozen class" % self )
        object.__setattr__(self, key, value)

    def _freeze(self):
        self.__isfrozen = True

class Test(FrozenClass):
    def __init__(self):
        self.x = 42#
        self.y = 2**3

        self._freeze() # no new attributes after this point.

a,b = Test(), Test()
a.x = 10
b.z = 10 # fails
Jochen Ritzel
źródło
Bardzo fajny! Myślę, że złapię ten kawałek kodu i zacznę go używać. (Hmm, zastanawiam się, czy można to zrobić jako dekorator, czy to nie byłby dobry pomysł ...)
weronika
5
Późny komentarz: Przez pewien czas z powodzeniem korzystałem z tego przepisu, dopóki nie zmieniłem atrybutu na właściwość, w której getter zgłaszał błąd NotImplementedError. Zajęło mi dużo czasu, zanim dowiedziałem się, że było to spowodowane tym, że faktycznie hasattrdzwoni getattr, odrzuca wynik i zwraca Fałsz w przypadku błędów, patrz ten blog . Znaleziono obejście poprzez zastąpienie not hasattr(self, key)przez key not in dir(self). To może być wolniejsze, ale rozwiązało problem.
Bas Swinckels,
31

Jeśli ktoś jest zainteresowany zrobieniem tego z dekoratorem, oto działające rozwiązanie:

from functools import wraps

def froze_it(cls):
    cls.__frozen = False

    def frozensetattr(self, key, value):
        if self.__frozen and not hasattr(self, key):
            print("Class {} is frozen. Cannot set {} = {}"
                  .format(cls.__name__, key, value))
        else:
            object.__setattr__(self, key, value)

    def init_decorator(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            func(self, *args, **kwargs)
            self.__frozen = True
        return wrapper

    cls.__setattr__ = frozensetattr
    cls.__init__ = init_decorator(cls.__init__)

    return cls

Całkiem proste w użyciu:

@froze_it 
class Foo(object):
    def __init__(self):
        self.bar = 10

foo = Foo()
foo.bar = 42
foo.foobar = "no way"

Wynik:

>>> Class Foo is frozen. Cannot set foobar = no way
Yoann Quenach de Quivillic
źródło
+1 dla wersji dekoratora. To właśnie użyłbym w przypadku większego projektu, w większym skrypcie to przesada (może gdyby mieli to w standardowej bibliotece ...). Na razie są tylko „ostrzeżenia w stylu IDE”.
Tomasz Gandor
2
Jak to rozwiązanie działa z dziedzictwem? np. jeśli mam klasę podrzędną Foo, to dziecko jest domyślnie zamrożoną klasą?
mrgiesel
Czy jest pakiet Pypi dla tego dekoratora?
winni2k
Jak można ulepszyć dekorator, aby działał w klasach dziedziczonych?
Ivan Nechipayko
30

Sloty to droga do zrobienia:

Pythonowym sposobem jest używanie automatów zamiast bawić się z __setter__. Chociaż może to rozwiązać problem, nie poprawia wydajności. Atrybuty obiektów przechowywane są w " __dict__" słowniku , dlatego można dynamicznie dodawać atrybuty do obiektów klas, które do tej pory stworzyliśmy. Używanie słownika do przechowywania atrybutów jest bardzo wygodne, ale może oznaczać stratę miejsca dla obiektów, które mają tylko niewielką liczbę zmiennych instancji.

Automaty to dobry sposób na obejście tego problemu zajmującego miejsce. Zamiast dynamicznego dyktowania, który umożliwia dynamiczne dodawanie atrybutów do obiektów, gniazda zapewniają statyczną strukturę, która zabrania dodawania atrybutów po utworzeniu instancji.

Kiedy projektujemy klasę, możemy użyć slotów, aby zapobiec dynamicznemu tworzeniu atrybutów. Aby zdefiniować gniazda, musisz zdefiniować listę z nazwą __slots__. Lista musi zawierać wszystkie atrybuty, których chcesz użyć. Pokazujemy to w następującej klasie, w której lista slotów zawiera tylko nazwę atrybutu „val”.

class S(object):

    __slots__ = ['val']

    def __init__(self, v):
        self.val = v


x = S(42)
print(x.val)

x.new = "not possible"

=> Nie udało się utworzyć atrybutu „nowy”:

42 
Traceback (most recent call last):
  File "slots_ex.py", line 12, in <module>
    x.new = "not possible"
AttributeError: 'S' object has no attribute 'new'

Uwaga:

  1. Od Pythona 3.3 korzyść polegająca na optymalizacji wykorzystania przestrzeni nie jest już tak imponująca. W Pythonie 3.3 do przechowywania obiektów używane są słowniki współdzielenia kluczy . Atrybuty instancji mogą współdzielić między sobą część ich wewnętrznej pamięci, tj. Część, która przechowuje klucze i odpowiadające im skróty. Pomaga to zmniejszyć zużycie pamięci przez programy, które tworzą wiele wystąpień typów niewbudowanych. Ale wciąż jest sposób, aby uniknąć dynamicznie tworzonych atrybutów.
  1. Korzystanie ze slotów wiąże się również z kosztami własnymi. Spowoduje to przerwanie serializacji (np. Marynowanie). Spowoduje to również przerwanie dziedziczenia wielokrotnego. Klasa nie może dziedziczyć z więcej niż jednej klasy, która definiuje sloty lub ma układ instancji zdefiniowany w kodzie C (np. List, tuple lub int).
DhiaTN
źródło
20

Właściwie nie chcesz __setattr__, chcesz __slots__. Dodaj __slots__ = ('foo', 'bar', 'baz')do treści klasy, a Python upewni się, że w każdej instancji są tylko foo, bar i baz. Ale przeczytaj zastrzeżenia na listach dokumentacji!


źródło
12
Używanie __slots__działa, ale złamie między innymi serializację (np. Pikle) ... Zwykle złym pomysłem jest używanie slotów do sterowania tworzeniem atrybutów, zamiast zmniejszać narzut pamięci, tak czy inaczej ...
Joe Kington
Wiem, i waham się przed użyciem tego samemu - ale robienie dodatkowej pracy, aby zablokować nowe atrybuty, też jest zazwyczaj złym pomysłem;)
2
Użycie __slots__również przerywa dziedziczenie wielokrotne. Klasa nie może dziedziczyć z więcej niż jednej klasy, albo Definiuje szczeliny lub nakrycie układ przykład te opisane w kodzie C (tak jak list, tuplealbo int).
Feuermurmel
Jeśli __slots__łamiesz marynaty, używasz starożytnego protokołu marynowania. Przejdź protocol=-1do metod pikle dla najnowszego dostępnego protokołu, czyli 2 w Pythonie 2 ( wprowadzonym w 2003 ). Domyślne i najnowsze protokoły w Pythonie 3 (odpowiednio 3 i 4) obsługują oba __slots__.
Nick Matteo
Cóż, przez większość czasu w ogóle żałuję
Erik Aronesty
7

Właściwym sposobem jest nadpisanie __setattr__. Po to tam jest.

Katriel
źródło
Jaki jest zatem właściwy sposób ustawiania zmiennych __init__? Czy jest to ustawienie ich __dict__bezpośrednio?
astrofrog
1
Przesłoniłbym __setattr__w __init__, przez self.__setattr__ = <new-function-that-you-just-defined>.
Katriel
6
@katrielalex: to nie zadziała dla klas w nowym stylu, ponieważ __xxx__metody są wyszukiwane tylko w klasie, a nie w instancji.
Ethan Furman
6

Bardzo podoba mi się rozwiązanie wykorzystujące dekorator, ponieważ jest łatwe w użyciu na wielu zajęciach w projekcie, z minimalnymi dodatkami dla każdej klasy. Ale nie działa dobrze w przypadku dziedziczenia. Oto moja wersja: zastępuje tylko funkcję __setattr__ - jeśli atrybut nie istnieje, a funkcja wywołująca nie jest __init__, wyświetla komunikat o błędzie.

import inspect                                                                                                                             

def froze_it(cls):                                                                                                                      

    def frozensetattr(self, key, value):                                                                                                   
        if not hasattr(self, key) and inspect.stack()[1][3] != "__init__":                                                                 
            print("Class {} is frozen. Cannot set {} = {}"                                                                                 
                  .format(cls.__name__, key, value))                                                                                       
        else:                                                                                                                              
            self.__dict__[key] = value                                                                                                     

    cls.__setattr__ = frozensetattr                                                                                                        
    return cls                                                                                                                             

@froze_it                                                                                                                                  
class A:                                                                                                                                   
    def __init__(self):                                                                                                                    
        self._a = 0                                                                                                                        

a = A()                                                                                                                                    
a._a = 1                                                                                                                                   
a._b = 2 # error
Eran Friedman
źródło
4

A co z tym:

class A():
    __allowed_attr=('_x', '_y')

    def __init__(self,x=0,y=0):
        self._x=x
        self._y=y

    def __setattr__(self,attribute,value):
        if not attribute in self.__class__.__allowed_attr:
            raise AttributeError
        else:
            super().__setattr__(attribute,value)
Clementerf
źródło
2

Oto podejście, które wymyśliłem, które nie wymaga atrybutu lub metody _frozen, aby freeze () w init.

Podczas inicjalizacji po prostu dodaję wszystkie atrybuty klasy do instancji.

Podoba mi się to, ponieważ nie ma funkcji _frozen, freeze (), a _frozen również nie pojawia się w danych wyjściowych vars (instancja).

class MetaModel(type):
    def __setattr__(self, name, value):
        raise AttributeError("Model classes do not accept arbitrary attributes")

class Model(object):
    __metaclass__ = MetaModel

    # init will take all CLASS attributes, and add them as SELF/INSTANCE attributes
    def __init__(self):
        for k, v in self.__class__.__dict__.iteritems():
            if not k.startswith("_"):
                self.__setattr__(k, v)

    # setattr, won't allow any attributes to be set on the SELF/INSTANCE that don't already exist
    def __setattr__(self, name, value):
        if not hasattr(self, name):
            raise AttributeError("Model instances do not accept arbitrary attributes")
        else:
            object.__setattr__(self, name, value)


# Example using            
class Dog(Model):
    name = ''
    kind = 'canine'

d, e = Dog(), Dog()
print vars(d)
print vars(e)
e.junk = 'stuff' # fails
gswilcox01
źródło
To nie wydaje się działać, jeśli jedno z pól jest listą. Powiedzmy names=[]. Następnie d.names.append['Fido']wstawi 'Fido'zarówno d.namesi e.names. Nie wiem wystarczająco dużo o Pythonie, aby zrozumieć, dlaczego.
Reinier Torenbeek
2

pystrictto instalowalny dekorator pypi zainspirowany tym pytaniem o przepełnienie stosu, którego można używać z klasami, aby je zamrozić. W pliku README znajduje się przykład, który pokazuje, dlaczego dekorator taki jak ten jest potrzebny, nawet jeśli w Twoim projekcie działa mypy i pylint:

pip install pystrict

Następnie użyj dekoratora @strict:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1
Erik Aronesty
źródło
1

Podoba mi się „Kraina lodu” Jochena Ritzela. Niewygodne jest to, że zmienna isfrozen następnie pojawia się podczas drukowania klasa .__ dict poszedłem wokół tego problemu w ten sposób, tworząc listę autoryzowanych atrybutów (podobnych do szczelin ):

class Frozen(object):
    __List = []
    def __setattr__(self, key, value):
        setIsOK = False
        for item in self.__List:
            if key == item:
                setIsOK = True

        if setIsOK == True:
            object.__setattr__(self, key, value)
        else:
            raise TypeError( "%r has no attributes %r" % (self, key) )

class Test(Frozen):
    _Frozen__List = ["attr1","attr2"]
    def __init__(self):
        self.attr1   =  1
        self.attr2   =  1
Arthur Bauville
źródło
1

FrozenClassJochen Ritzel jest cool, ale nazywając _frozen()gdy initialing klasę za każdym razem nie jest tak fajnie (i trzeba podjąć ryzyko zapominając go). Dodałem __init_slots__funkcję:

class FrozenClass(object):
    __isfrozen = False
    def _freeze(self):
        self.__isfrozen = True
    def __init_slots__(self, slots):
        for key in slots:
            object.__setattr__(self, key, None)
        self._freeze()
    def __setattr__(self, key, value):
        if self.__isfrozen and not hasattr(self, key):
            raise TypeError( "%r is a frozen class" % self )
        object.__setattr__(self, key, value)
class Test(FrozenClass):
    def __init__(self):
        self.__init_slots__(["x", "y"])
        self.x = 42#
        self.y = 2**3


a,b = Test(), Test()
a.x = 10
b.z = 10 # fails
Endle_Zhenbo
źródło