Search code examples
pythonmultiple-inheritance

multiple python class inheritance


I am trying to understand python's class inheritance methods and I have some troubles figuring out how to do the following:

How can I inherit a method from a class conditional on the child's input?

I have tried the following code below without much success.

class A(object):
    def __init__(self, path):
        self.path = path

    def something(self):
        print("Function %s" % self.path)   


class B(object):
    def __init__(self, path):
        self.path = path
        self.c = 'something'

    def something(self):
        print('%s function with %s' % (self.path, self.c))


class C(A, B):
    def __init__(self, path):
        # super(C, self).__init__(path)

        if path=='A':
            A.__init__(self, path)
        if path=='B':
            B.__init__(self, path)
        print('class: %s' % self.path)


if __name__ == '__main__':
    C('A')
    out = C('B')
    out.something()

I get the following output:

class: A
class: B
Function B

While I would like to see:

class: A
class: B
B function with something

I guess the reason why A.something() is used (instead of B.something()) has to do with the python's MRO.


Solution

  • Calling __init__ on either parent class does not change the inheritance structure of your classes, no. You are only changing what initialiser method is run in addition to C.__init__ when an instance is created. C inherits from both A and B, and all methods of B are shadowed by those on A due to the order of inheritance.

    If you need to alter class inheritance based on a value in the constructor, create two separate classes, with different structures. Then provide a different callable as the API to create an instance:

    class CA(A):
        # just inherit __init__, no need to override
    
    class CB(B):
        # just inherit __init__, no need to override
    
    def C(path):
        # create an instance of a class based on the value of path
        class_map = {'A': CA, 'B': CB}
        return class_map[path](path)
    

    The user of your API still has name C() to call; C('A') produces an instance of a different class from C('B'), but they both implement the same interface so this doesn't matter to the caller.

    If you have to have a common 'C' class to use in isinstance() or issubclass() tests, you could mix one in, and use the __new__ method to override what subclass is returned:

    class C:
        def __new__(cls, path):
            if cls is not C:
                # for inherited classes, not C itself
                return super().__new__(cls)
            class_map = {'A': CA, 'B': CB}
            cls = class_map[path]
            # this is a subclass of C, so __init__ will be called on it
            return cls.__new__(cls, path)
    
    class CA(C, A):
        # just inherit __init__, no need to override
        pass
    
    class CB(C, B):
        # just inherit __init__, no need to override
        pass
    

    __new__ is called to construct the new instance object; if the __new__ method returns an instance of the class (or a subclass thereof) then __init__ will automatically be called on that new instance object. This is why C.__new__() returns the result of CA.__new__() or CB.__new__(); __init__ is going to be called for you.

    Demo of the latter:

    >>> C('A').something()
    Function A
    >>> C('B').something()
    B function with something
    >>> isinstance(C('A'), C)
    True
    >>> isinstance(C('B'), C)
    True
    >>> isinstance(C('A'), A)
    True
    >>> isinstance(C('A'), B)
    False
    

    If neither of these options are workable for your specific usecase, you'd have to add more routing in a new somemethod() implementation on C, which then calls either A.something(self) or B.something(self) based on self.path. This becomes cumbersome really quickly when you have to do this for every single method, but a decorator could help there:

    from functools import wraps
    
    def pathrouted(f):
        @wraps
        def wrapped(self, *args, **kwargs):
            # call the wrapped version first, ignore return value, in case this
            # sets self.path or has other side effects
            f(self, *args, **kwargs)
            # then pick the class from the MRO as named by path, and call the
            # original version
            cls = next(c for c in type(self).__mro__ if c.__name__ == self.path)
            return getattr(cls, f.__name__)(self, *args, **kwargs)
        return wrapped
    

    then use that on empty methods on your class:

    class C(A, B):
        @pathrouted
        def __init__(self, path):
            self.path = path
            # either A.__init__ or B.__init__ will be called next
    
        @pathrouted
        def something(self):
            pass  # doesn't matter, A.something or B.something is called too
    

    This is, however, becoming very unpythonic and ugly.