Zapobieganie tworzeniu nowych atrybutów poza init

Chcę być w stanie stworzyć klasę (w Pythonie), która raz zainicjowana __init__ nie akceptuje nowych atrybutów, ale akceptuje modyfikacje istniejących atrybutów. Jest kilka sposobów na to, jak to zrobić, na przykład posiadanie metody __setattr__, takiej jak

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

A następnie edytować __dict__ bezpośrednio w __init__, ale zastanawiałem się, czy jest na to "właściwy" sposób?

Author: DhiaTN, 2010-08-30

11 answers

Nie używałbym bezpośrednio __dict__, ale możesz dodać funkcję do jawnego "zamrożenia" instancji:

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
 56
Author: Jochen Ritzel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-30 20:04:00

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

 17
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-30 19:38:39

Jeśli ktoś jest zainteresowany zrobieniem tego z dekoratorem, oto rozwiązanie robocze:

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 prosty 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
 16
Author: Yoann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-31 12:26:13

Slots is the way to go:

Sposobem pythonicznym jest używanie slotów zamiast bawić się __setter__. Chociaż może rozwiązać problem, nie daje żadnej poprawy wydajności. Atrybuty obiektów są przechowywane w słowniku " __dict__", 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ć marnowanie miejsca na obiekty, które mają tylko niewielką ilość zmiennych instancji.

Sloty są dobrym sposobem obejścia tego problemu zużycia przestrzeni. Zamiast mieć dynamiczny dict, który umożliwia dynamiczne dodawanie atrybutów do obiektów, sloty zapewniają statyczną strukturę, która zakazuje dodawania po utworzeniu instancji.

Kiedy projektujemy klasę, możemy użyć slotów, aby zapobiec dynamicznemu tworzeniu atrybutów. Aby zdefiniować sloty, musisz zdefiniować listę z nazwą __slots__. Lista musi zawierać wszystkie atrybuty, których chcesz użyć. Zademonstrujemy 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 tworzy atrybutu "new":

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 wersji Python 3.3 zaleta optymalizacji zużycia przestrzeni nie jest już tak imponująca. Z Pythonem 3.3 Key-Sharing słowniki są używane do przechowywanie przedmiotów. Atrybuty instancji mogą współdzielić część pamięci wewnętrznej między sobą, tj. część, która przechowuje klucze i odpowiadające im skróty. Pomaga to zmniejszyć zużycie pamięci programów, które tworzą wiele instancji nie wbudowanych typów. Ale nadal jest sposób, aby uniknąć dynamicznie tworzonych atrybutów.

  2. Korzystanie z automatów wiąże się również z własnym kosztem. Złamie serializację(np. ogórek). Pęknie też wielokrotne dziedziczenie. Klasa nie może dziedziczyć z więcej niż jednej klasy, która albo definiuje sloty, albo zawiera układ instancji zdefiniowany w kodzie C (jak list, tuple lub int).

 12
Author: DhiaTN,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-08 10:55:41

Właściwym sposobem jest obejście __setattr__. Po to tu jest.

 6
Author: Katriel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-30 19:22:54

Bardzo podoba mi się rozwiązanie, które wykorzystuje dekorator, ponieważ jest łatwy w użyciu dla wielu klas w całym projekcie, z minimalnymi dodatkami dla każdej klasy. Ale to nie działa dobrze z dziedziczeniem. Oto moja wersja: nadpisuje 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
 4
Author: Eran Friedman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-27 18:57:02

Oto podejście, które wymyśliłem, które nie potrzebuje atrybutu _frozen ani metody do zamrożenia() w init.

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

Podoba mi się to, ponieważ nie ma _frozen, freeze (), a _frozen również nie pojawia się w wyjściu 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
 2
Author: gswilcox01,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-19 17:47:49

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)
 2
Author: Clementerf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-25 03:17:34

Lubię "zamrożone" Jochena Ritzela. Niewygodne jest to, że zmienna isfrozen pojawia się wtedy podczas drukowania klasy.__dict Obejrzałem ten problem w ten sposób, tworząc listę autoryzowanych atrybutów (podobnych do slotów ):

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
 1
Author: Arthur Bauville,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-06 04:14:58

FrozenClass Jochena Ritzela jest fajne, ale wywołanie _frozen() podczas inicjowania klasy za każdym razem nie jest takie fajne (i musisz zaryzykować, że o tym zapomnisz). 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
 1
Author: Endle_Zhenbo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-31 08:10:22

Ulepszeniem tego doskonałym rozwiązaniem za pomocą __slots__ byłoby pozwolić metodzie __init__ zdefiniować dozwolone atrybuty, a następnie ustawić __slots__ atrybut z bieżącą klasą __dict__

Zaletą jest to, że unika podwójnej aktualizacji przy dodawaniu atrybutu w __init__:

class A:
    def __init__(self):
        self.a = 12
        self.b = 34
        self.__slots__ = self.__dict__


a = A()
a.b = 33   # okay
a.c = 22   # AttributeError: 'A' object has no attribute 'c'
 -1
Author: Jean-François Fabre,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-13 13:57:27