I would like to prevent a user of an instance mistakenly creating a non existing attribute when using said object.
Say I have a class with an init and some attributes :
class Foo(obj):
def __init__(self, a, b):
self.a = a
self.b = b
I would like to enable setting existing attributes while preventing the creation of a new attribute:
myobj = foo(1,2)
>>
print(myobj.a)
>> 1
myobj.a = 2
myobj.c = 1
>> AttributeError: frozen instance
That is fairly easy with __setattr__
override and a boolean :
class Foo(obj):
_is_frozen = False
def __init__(self, a, b):
self.a = a
self.b = b
self._is_frozen = True
def __setattr__(self, name, value):
if not self._is_frozen or hasattr(self, name):
super().__setattr__(name, value)
else:
raise AttributeError("frozen instance")
Now the step I struggle with is when a new class inherits Foo. If new attributes must be defined after the call to the super().__init__()
, the instance is frozen.
I have tried using decorator to make a metaclass but the decorator of the mother class is still called at the super().__init__()
call and I can't define my new attributes.
In other words, is there a way to make a kind of __post_init__
method (reference to the dataclass module) that would only be called after all inits (the one of the class and the ones of the inherited classes) have been called ?
CodePudding user response:
Indeed - that is tricky.
Having a decorator on the __init__
of base class that would freeze the instance after __init__
, as you mentioned have the problem of state - it is possible to add other state variables, or count the __init__
depth, and use a metaclass (or __init_subclass__
) to decorate all __init__
method in the subclasses, so that it would do the freezing only when exiting the outermost __init__
.
But there is an easier way using metaclasses: the metaclass' __call__
method is what calls a class __new__
and then __init__
when creating a new instance. So, it suffices to put code to calling a __post_init__
on a custom metaclass' __call__
(or freeze the instance directly from it)
class PostInitMeta(type):
def __call__(cls, *args, **kw):
instance = super().__call__(*args, **kw) # < runs __new__ and __init__
instance.__post_init__()
return instance
class Freezing(metaclass=PostInitMeta):
_frozen = False
def __post_init__(self):
self._frozen = True
def __setattr__(self, name, value):
if self._frozen:
raise AttributeError() # or do nothing, and return as you prefer
super().__setattr__(name, value)
class A(Freezing):
def __init__(self, a):
self.a = a
class B(A):
def __init__(self, a, b):
super().__init__(a)
self.b = b
And testing this in interactive mode:
In [18]: b = B(2, 3)
In [19]: b.a
Out[19]: 2
In [20]: b.b
Out[20]: 3
In [21]: b.b = 5
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In [21], line 1
----> 1 b.b = 5