Search code examples
python-3.xoopinheritancedynamicmetaclass

dynamic inheritance with type and super


I'm looking for a way to dynamically inherit a parent class with its attributes and methods, by using type for class creation and super for inheritance, like so:

class A:
    def __init__(self,a,b):
        self.a = a
        self.b = b
    
    def some_method(self,q):
        return (self.a + self.b)**q

def B_init(self,**kwargs):
    super().__init__(**kwargs)

def another_method(self,):
    return 1

def class_B_factory(parent_class):
    return type(
        'B',
        (parent_class, some_other_parent_class),
        {'__init__':B_init,
         'another_method':another_method
        }
               )

And then be able to call...

model = class_B_factory(A)(a = 1, b = 5)
print(model.some_method(2)) # outputs to (1 + 5)**2 = 36

I'm not sure how to proceed. I don't think I'll need a custom metaclass since I'm pretty sure you can't call the parent class' __init__ method while also creating self in the process. I also tried overriding the default __init__ method outside the scope of class_B_factory like so:


def class_B_factory(parent_class):
    return type(
        'B',
        (parent_class, some_other_parent_class),
        {'another_method':another_method
        }
               )

B = class_B_factory(A)

def B_init(self,**kwargs):
    super(B,self).__init__(**kwargs)

B.__init__ = B_init

model = B(a = 1, b = 5)

because I figured type doesn't need __init__ right away, as it is only needed during instantiation. But then I get TypeError: __init__() got an unexpected keyword argument error, which seems like it didn't work, and its not clean anyway.

EDIT: I tried defining the methods outside the factory via the following but I am still unsuccessful. Not sure how to fix it. Python has trouble instantiating maybe?

class A:
    ...

def B_init(self, produced_class = None, **kwargs):
    super(produced_class,self).__init__(**kwargs)

def another_method(self, q, parent_class = None):
    if parent_class is not None:
        return 3 * parent_class.some_method(self,q) # I expect any parent_class passed to have a method called some_method
    return 1

def class_B_factory(parent_class, additional_methods):
    methods = {}
    for name, method in additional_methods.items():
        if "parent_class" in signature(method).parameters:
            method = partial(method, parent_class = parent_class) # freeze the parent_class argument, which is a cool feature
        methods[name] = method
    
    newcls = type(
            'B',
            (parent_class,),
            methods # would not contain B_init
                 )
    
    newcls.__init__ = partial(B_init, produced_class = newcls) # freeze the produced class that I am trying to fabricate into B_init here
    
    return newcls


model = class_B_factory(parent_class = A, additional_methods = {"another_method": another_method})

print(signature(model.__init__).parameters) # displays OrderedDict([('self', <Parameter "self">),...]) so it contains self!

some_instance_of_model = model(a = 1, b = 5) # throws TypeError: B_init() missing 1 required positional argument: 'self'

Solution

  • The parameterless form of super() relies on it being physically placed inside a class body - the Python machinnery them will, under the hood, create a __class__ cell variable referring that "physical" class (roughly equivalent to a non-local variable), and place it as the first parameter in the super() call.

    For methods not written inside class statements, one have to resort to explicitly placing the parameters to super, and these are the child class, and the instance (self).

    The easier way to do that in your code is to define the methods inside your factory function, so they can share a non-local variable containing the newly created class in the super call: ​

    
    
    def class_B_factory(parent_class):
    
        def B_init(self,**kwargs):
            nonlocal newcls  # <- a bit redundant, but shows how it is used here
            ​super(newcls, self).__init__(**kwargs)
    
        def another_method(self,):
            ​​return 1
    
       ​ newcls = type(
           ​'B',
           ​(parent_class, some_other_parent_class),
           ​{'__init__':B_init,
            ​'another_method':another_method
           ​}
        return newcls
    

    If you have to define the methods outside of the factory function (which is likely), you have to pass the parent class into them in some form. The most straightforward would be to add a named-parameter (say __class__ or "parent_class"), and use functools.partial inside the factory to pass the parent_class to all methods in a lazy way:

    
    from functools import partial
    from inspect import signature
    
    class A:
        ...
    
    
    # the "parent_class" argument name is given a special treatement in the factory function:
    def B_init(self, *, parent_class=None, **kwargs):
        nonlocal newcls  # <- a bit redundant, but shows how it is used here
        ​super([parent_class, self).__init__(**kwargs)
    
    def another_method(self,):
        ​​return 1
    
    
    def class_B_factory(parent_class, additional_methods, ...):
        methods = {}
        for name, method in additional_methods.items():
            if "parent_class" in signature(method).parameters:
                method = partial(method, parent_class=parent_class)
            # we populate another dict instead of replacing methods
            # so that we create a copy and don't modify the dict at the calling place.
            methods[name] = method
        
    
       ​ newcls = type(
           ​'B',
           ​(parent_class, some_other_parent_class),
           methods
        )
        return newcls
    
    new_cls = class_B_factory(B, {"__init__": B_init, "another_method": another_method})