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?
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
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!
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
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:
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.
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).
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.
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
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
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)
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
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
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'
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