Home > Enterprise >  Is there a way to implement a __post_init__ method for inherited classes?
Is there a way to implement a __post_init__ method for inherited classes?

Time:09-16

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


  • Related