Verhindern Sie das Erstellen neuer Attribute außerhalb von __init__

80

Ich möchte in der Lage sein, eine Klasse (in Python) zu erstellen, die nach der Initialisierung __init__keine neuen Attribute akzeptiert, aber Änderungen an vorhandenen Attributen akzeptiert. Es gibt verschiedene hack-ish Möglichkeiten, wie ich dies sehen kann, zum Beispiel eine __setattr__Methode wie

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

und dann __dict__direkt im Inneren bearbeiten __init__, aber ich habe mich gefragt, ob es einen "richtigen" Weg gibt, dies zu tun?

Astrofrog
quelle
1
Katrielalex bringt gute Punkte. Daran ist nichts Hackiges. Sie könnten die Verwendung vermeiden, __setattr__aber das wäre wahrscheinlich hacky.
Aaronasterling
Ich verstehe nicht, warum das hacky ist? Es ist die beste Lösung, die ich finden konnte, und viel prägnanter als einige der anderen vorgeschlagenen.
Chris B

Antworten:

76

Ich würde nicht __dict__direkt verwenden, aber Sie können eine Funktion hinzufügen, um eine Instanz explizit "einzufrieren":

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
quelle
Sehr cool! Ich denke, ich werde mir diesen Code schnappen und ihn verwenden. (Hmm, ich frage mich, ob es als Dekorateur gemacht werden könnte, oder ob das keine gute Idee wäre ...)
weronika
5
Später Kommentar: Ich habe dieses Rezept einige Zeit erfolgreich verwendet, bis ich ein Attribut in eine Eigenschaft geändert habe, in der der Getter einen NotImplementedError ausgelöst hat. Es hat lange hasattrgedauert getattr, bis ich herausgefunden habe, dass dies auf die Tatsache zurückzuführen ist, dass tatsächlich aufgerufen wird, das Ergebnis weggeworfen wird und im Fehlerfall False zurückgegeben wird (siehe diesen Blog) . Es wurde eine Problemumgehung gefunden, indem durch ersetzt not hasattr(self, key)wurde key not in dir(self). Dies mag langsamer sein, hat aber das Problem für mich gelöst.
Bas Swinckels
30

Wenn jemand daran interessiert ist, dies mit einem Dekorateur zu tun, ist hier eine funktionierende Lösung:

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

Ziemlich einfach zu bedienen:

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

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

Ergebnis:

>>> Class Foo is frozen. Cannot set foobar = no way
Yoann Quenach de Quivillic
quelle
+1 für die Dekorateurversion. Das würde ich für ein größeres Projekt verwenden, in einem größeren Skript ist dies übertrieben (vielleicht, wenn sie es in der Standardbibliothek hätten ...). Derzeit gibt es nur "Warnungen im IDE-Stil".
Tomasz Gandor
2
Wie funktioniert diese Lösung mit dem Erbe? Beispiel: Wenn ich eine untergeordnete Klasse von Foo habe, ist dieses Kind standardmäßig eine eingefrorene Klasse.
Mrgiesel
Gibt es ein Pypi-Paket für diesen Dekorateur?
Winni2k
Wie kann man den Dekorateur so verbessern, dass er für geerbte Klassen funktioniert?
Ivan Nechipayko
26

Slots ist der richtige Weg:

Die pythonische Methode besteht darin, Slots zu verwenden, anstatt mit dem herumzuspielen __setter__. Während es das Problem lösen kann, gibt es keine Leistungsverbesserung. Die Attribute von Objekten werden in einem Wörterbuch " __dict__" gespeichert. Aus diesem Grund können Sie Objekten von Klassen, die wir bisher erstellt haben, dynamisch Attribute hinzufügen. Die Verwendung eines Wörterbuchs zur Speicherung von Attributen ist sehr praktisch, kann jedoch Platzverschwendung für Objekte bedeuten, die nur eine geringe Anzahl von Instanzvariablen enthalten.

Steckplätze sind eine gute Möglichkeit, um dieses Platzverbrauchsproblem zu umgehen. Anstatt ein dynamisches Diktat zu haben, mit dem Objekte dynamisch zu Objekten hinzugefügt werden können, bieten Slots eine statische Struktur, die das Hinzufügen nach dem Erstellen einer Instanz verhindert.

Wenn wir eine Klasse entwerfen, können wir Slots verwenden, um die dynamische Erstellung von Attributen zu verhindern. Um Slots zu definieren, müssen Sie eine Liste mit dem Namen definieren __slots__. Die Liste muss alle Attribute enthalten, die Sie verwenden möchten. Wir demonstrieren dies in der folgenden Klasse, in der die Slots-Liste nur den Namen für ein Attribut "val" enthält.

class S(object):

    __slots__ = ['val']

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


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

x.new = "not possible"

=> Es kann kein Attribut "neu" erstellt werden:

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'

NB:

  1. Seit Python 3.3 ist der Vorteil der Optimierung des Platzverbrauchs nicht mehr so ​​beeindruckend. Mit Python 3.3 werden Key-Sharing- Wörterbücher zum Speichern von Objekten verwendet. Die Attribute der Instanzen können einen Teil ihres internen Speichers untereinander teilen, dh den Teil, in dem die Schlüssel und die entsprechenden Hashes gespeichert sind. Dies hilft, den Speicherverbrauch von Programmen zu reduzieren, wodurch viele Instanzen nicht integrierter Typen erstellt werden. Es ist jedoch immer noch der richtige Weg, um dynamisch erstellte Attribute zu vermeiden.

  2. Die Verwendung von Slots ist auch mit eigenen Kosten verbunden. Die Serialisierung wird unterbrochen (z. B. Pickle). Es wird auch Mehrfachvererbung brechen. Eine Klasse kann nicht von mehr als einer Klasse erben, die entweder Slots definiert oder ein im C-Code definiertes Instanzlayout hat (wie Liste, Tupel oder Int).

DhiaTN
quelle
20

Eigentlich willst du nicht __setattr__, du willst __slots__. Fügen Sie __slots__ = ('foo', 'bar', 'baz')dem Klassenkörper hinzu, und Python stellt sicher, dass auf jeder Instanz nur foo, bar und baz vorhanden sind. Aber lesen Sie die Vorbehalte in den Dokumentationslisten!


quelle
12
Die Verwendung von __slots__Werken, aber es wird unter anderem die Serialisierung (z. B. Pickle) unterbrechen ... Es ist normalerweise eine schlechte Idee, Slots zu verwenden, um die Attributerstellung zu steuern, anstatt den Speicheraufwand meiner Meinung nach zu verringern ...
Joe Kington
Ich weiß, und ich hoffe, es selbst zu verwenden - aber zusätzliche Arbeit zu leisten, um neue Attribute nicht zuzulassen, ist normalerweise auch eine schlechte Idee;)
2
Die Verwendung unterbricht __slots__auch die Mehrfachvererbung. Eine Klasse kann nicht von mehr als einer Klasse erben , dass entweder definiert Schlitze oder Hut ein Beispiel - Layout in C - Code definiert ist (wie list, tupleoder int).
Feuermurmel
Wenn __slots__Ihre Gurken kaputt gehen, verwenden Sie ein altes Gurkenprotokoll. Übergeben Sie protocol=-1die Pickle-Methoden für das neueste verfügbare Protokoll, nämlich 2 in Python 2 ( eingeführt 2003 ). Das Standardprotokoll und das neueste Protokoll in Python 3 (3 bzw. 4) werden beide verarbeitet __slots__.
Nick Matteo
Nun
ich es,
7

Der richtige Weg ist zu überschreiben __setattr__. Dafür ist es da.

Katriel
quelle
Was ist dann der richtige Weg, um Variablen zu setzen __init__? Ist es, sie __dict__direkt einzuschalten?
Astrofrog
1
Ich würde außer Kraft setzt __setattr__in __init__, durch self.__setattr__ = <new-function-that-you-just-defined>.
Katriel
6
@katrielalex: Das funktioniert nicht für Klassen neuen Stils, da __xxx__Methoden nur für die Klasse und nicht für die Instanz nachgeschlagen werden.
Ethan Furman
6

Ich mag die Lösung, die einen Dekorateur verwendet, sehr, weil es einfach ist, sie für viele Klassen in einem Projekt zu verwenden, mit minimalen Ergänzungen für jede Klasse. Aber es funktioniert nicht gut mit Vererbung. Hier ist meine Version: Sie überschreibt nur die Funktion __setattr__. Wenn das Attribut nicht vorhanden ist und die Aufruferfunktion nicht __init__ ist, wird eine Fehlermeldung ausgegeben.

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

Was ist damit:

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

Hier ist ein Ansatz, den ich mir ausgedacht habe und der kein _frozen-Attribut oder keine Methode benötigt, um () in init einzufrieren.

Während der Initiale füge ich einfach alle Klassenattribute zur Instanz hinzu.

Ich mag das, weil es kein _frozen gibt, freeze () und _frozen auch nicht in der Ausgabe von vars (instance) angezeigt wird.

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
quelle
Dies scheint nicht zu funktionieren, wenn eines der Felder eine Liste ist. Sagen wir mal names=[]. Dann d.names.append['Fido']wird 'Fido'in beide d.namesund eingefügt e.names. Ich weiß nicht genug über Python, um zu verstehen, warum.
Reinier Torenbeek
2

pystrictist ein installierbarer Pypi-Dekorateur, der von dieser Stackoverflow-Frage inspiriert wurde und mit Klassen zum Einfrieren verwendet werden kann. In der README gibt es ein Beispiel, das zeigt, warum ein solcher Dekorateur benötigt wird, auch wenn in Ihrem Projekt mypy und pylint ausgeführt werden:

pip install pystrict

Verwenden Sie dann einfach den @ strict-Dekorator:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1
Erik Aronesty
quelle
1

Ich mag das "Frozen" von Jochen Ritzel. Das Unpraktische ist, dass die Variable isfrozen dann beim Drucken einer Klasse angezeigt wird .__ dikt Ich habe dieses Problem auf diese Weise umgangen, indem ich eine Liste autorisierter Attribute (ähnlich wie bei Slots ) erstellt habe:

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

Das FrozenClassvon Jochen Ritzel ist cool, aber _frozen()jedes Mal beim Initialisieren einer Klasse anzurufen ist nicht so cool (und Sie müssen das Risiko eingehen, es zu vergessen). Ich habe eine __init_slots__Funktion hinzugefügt :

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
quelle