Dynamically adding inheritances

I’ve been losing my mind over something that is possibly very plain.
A class is called which inherits a class dynamically from its keywords. Adding a helper class is likely the way to go. This is a nonworking illustration:

class Third:
    def __init__(self, *args, **kwds):
        self.string = 'XYZ'

class Agent(Third):
    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)

class Main:
    def __init__(self, *args, **kwds):
        if kwds.get('third_please'):
            super(Agent, self).__init__(*args, **kwds)   # Wrong usage.
            print(self.string)   # desired is 'XYZ'
            
Main(third_please=True)

This is based on an SO example and works when hard-coding the type call (Agent = type(‘Agent’, (Third,), {}). The problem is I want to supply ‘Third’ dynamically from another class call e.g. CallingClass(third_requested=True).
That is, I’d like for Main to inherit either Third or Fourth depending on keywords.
Adding additional classes is likely required but I’m unsure how to go about it.

class Fourth:
    def __init__(self, *args, **kwds):
        self.string = 'DEF'

class Third:
    def __init__(self, *args, **kwds):
        self.string = 'XYZ'

Agent = type('Agent', (Third,), {})
def __init__(self, *args, **kwds):
    super(Agent, self).__init__(*args, **kwds)
    Agent.__init__ = __init__

class Main(Agent):
    def __init__(self, *args, **kwds):
        if kwds.get('third_one_requested'):
            super().__init__(*args, **kwds)
            print(self.string)

Main(third_requested=True)   # This is the main call

Is it okay if Main isn’t technically a class, but is a factory function? Python classes, being first-class objects themselves, can be constructed at the time they’re needed. You could have your Main function look at its keyword arguments, figure out whether an appropriate class exists, and if not, builds the class with the necessary inheritance tree.

This WILL mean that there are multiple classes with the same name but different method resolution orders. That’s not a problem for Python, but make sure it won’t be a problem for you as a programmer.

BTW, I would strongly recommend (if possible) keeping all the other classes the same and just having the leaf class differ. This can be done by adding one more level to the inheritance, and then using multiple inheritance:

class Root: pass
class Fourth(Root):
    ...
class Third(Root):
    ...
class Agent(Root):
    ...

class MainThird(Agent, Third):
    ...
class MainFourth(Agent, Fourth):
    ...

Observe:

>>> MainThird.__mro__
(<class '__main__.MainThird'>, <class '__main__.Agent'>, <class '__main__.Third'>, <class '__main__.Root'>, <class 'object'>)
>>> MainFourth.__mro__
(<class '__main__.MainFourth'>, <class '__main__.Agent'>, <class '__main__.Fourth'>, <class '__main__.Root'>, <class 'object'>)

Inside Agent(), calling super().__init__(*args, **kwds) will pass the call on to “whichever class is next in the MRO”, which will correctly hand it to either Third or Fourth, without Agent itself needing any changes. (Note that Root doesn’t need any functionality of its own, but if there’s any code common to Third and Fourth, it’s the perfect place for it.)

Your Main function could then look like this:

def Main(*args, **kwds):
    if kwds.get("third_one_requested"): return MainThird(*args, **kwds)
    return MainFourth(*args, **kwds)

or even something more elaborate:

_class_cache = {}
def Main(*args, **kwds):
    which = kwds.pop("which")
    if which not in _class_cache:
        _class_cache[which] = type("Main", (Agent, globals()[which], {})
    return _class_cache[which](*args, **kwds)

Would that serve?

2 Likes

Thanks a lot. I have something functional thanks to your example.

Just to illustrate what kind of spaghetti can be created with tunnel vision, it doesn’t throw but good luck importing or using it in practice:

class Third:
    def __init__(self, *args, **kwds):
        self.string = 'Pomodoro'

class Fourth:
    def __init__(self, *args, **kwds):
        self.string = 'Pesto'        
        
class EntryClass:
    def __init__(self, *args, **kwds):
        if kwds.get('Third_requested'):
            self.kwds = kwds
        if kwds.get('Fourth_requested'):
            self.kwds = kwds

class GetClass:
    def __init__(self, *args, **kwds):
        if kwds.get('Third_requested'):
            self.name = Third
        if kwds.get('Fourth_requested'):
            self.name = Fourth

entry_class = EntryClass(Third_requested=True)

Agent = type('Agent', (GetClass(**entry_class.kwds).name,), {})
def __init__(self, *args, **kwds):
    super(Agent, self).__init__(*args, **kwds)
Agent.__init__ = __init__

class Main(Agent):
    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        print(self.string)
Main()