0

For a minimalist example, say I have three classes:

class A():
    def method(self):
        print('A')
        self._method()
    def _method(self):
        pass

class B(A):
    def _method(self):
        print('B')

class C(A):
    def _method(self):
        print('C')

I use them as, e.g.,

c = C()
c.method()

Now, I want to have a class D that modifies the behavior of class A's method.

class D():
    def method(self):
        self._method()
        print('D')
    def _method(self):
        pass

and similarly, I'd like for B and C to be able to inherit from D.

There are a few ways to accomplish this: I can create new D classes that inherit from B and C:

class D_B(B):
    def method(self):
        self._method()
        print('D')

class D_C(C):
    def method(self):
        self._method()
        print('D')

Or I can create new B and C classes that inherit from D, instead of from A.

class B_D(D):
    def _method(self):
        print('B')

class C_D(D):
    def _method(self):
        print('C')

Both of these solutions involve having fairly redundant code. In the first one, D_B and D_C have exactly the same method definition. In the latter, B_D and C_D have the same _method definitions as B and C. So, generally, it seems like this should be something I should be able to accomplish in a better way, but I am not familiar with how. I should also note that it's not just a matter of "this looks redundant and feels bad". In reality, the number of "leaf" (B and C) classes is a lot higher than 2, and the number of "root" (A and D) may also grow. It would be maintenance hell to have to modify every "copy" of class for every change.

Another way to implement this is to directly modify "A" such that it contains both desired behaviors, controlled e.g., by a flag in initialization:

class A():
    def __init__(self, flag = True):
        self.flag = flag
    def method(self):
        if self.flag:
            print('A')
            self._method()
        else:
            self._method()
            print('D')
    def _method(self):
        pass

So far, this is my preferred approach, but I am wondering if it is possible to have the same thing, but with separate classes instead. Specifically, I would like to, somehow, be able to tell the parent class when constructing or initializing my instance, that is, I'd like to do something like:

b = B(A)
c = C(D)

so that b is created as an instance of B that inherits the method of A, while c is created as an instance of C that inherits the method of B. Is this possible? Is there a specific name for what I'm trying to do?

1
  • 1
    I want to say this feels like composition but this is hard for me to follow, so I may be off-base. Commented Feb 13 at 15:56

2 Answers 2

2

The way multiple inheritance works in Python makes it ready to work with what you are asking for.

This means: a class that has D in the inheritance chain before B will have the D methods called, and whatever method in D which refers to a method redefined in B will use that.

In other words, after:

class A():
    def method(self):
        print('A')
        self._method()
    def _method(self):
        pass

class B(A):
    def _method(self):
        print('B')

class C(A):
    def _method(self):
        print('C')

class D(A):  # Note that D has to inherit from A. 
    def method(self):
        self._method()
        print('D')
    def _method(self):
        pass

If you will need a family of classes using D's method and their own _method, do:

class B_D(B,D):
   pass

This will output:

In [26]: B_D().method()
B
D

This arrangement creates a B_D class with mro == (__main__.B_D, __main__.B, __main__.D, __main__.A, object) : so a method not found in B is searched in D then in A.
Note that no code is needed in the body of the derived class making use of multiple inheritance.

Sign up to request clarification or add additional context in comments.

Comments

2

I think this case warrants better use of composition rather then what you are trying to do - inheritance. I'd suggest using typing.Protocol to ensure your A, B, C and D classes implement correct methods, and then constructing objects of either B and C using instances of A or D, as needed, as a attribute rather than parent class. This way you can control it when instancing the object, rather than doing it at class initialization.

Defining protocols:

from typing import Protocol

class HasFoo(Protocol):
    def foo(self) -> None:
        pass

class HasBar(Protocol):
    fooer: HasFoo
    def __init__(self, fooer: HasFoo):
        self.fooer = fooer
    def bar(self) -> None:
        pass

Implementing classes using protocols:

class A(HasFoo):
    def foo(self):
        print("fooA")

class D(HasFoo):
    def foo(self):
        print("fooD")

class B(HasBar):
    def bar(self):
        print("barB")
        self.fooer.foo()

class C(HasBar):
    def bar(self):
        print("barC")
        self.fooer.foo()

Constructing the objects:

a = A()
d = D()

b_a = B(fooer=a)
b_d = B(fooer=d)
c_d = C(fooer=d)

print("b_a:")
b_a.bar()
print("b_d:")
b_d.bar()
print("c_d:")
c_d.bar()

Output:

b_a:
barB
fooA
b_d:
barB
fooD
c_d:
barC
fooD

3 Comments

I think involving typing.Protocol here doesn't really add anything if the question doesn't really involve static analysis or typing.
The reason I was going for inheritance (which I didn't make clear in my over-simplified example, so mb in there :D) is that foo in both A and D invoke other methods from B and C, as well. But ultimately I think you are right - the simpler way to do it is to have A/D take an instance of B/C and use their methods then, rather than A and D being parents to B and C.
nope - your use case is one of the use cases for multiple inheritance. Yes, it is hard, and had to be done carefully - don't forget to test! - But other than that, once you put things in the correct order it will work as you need out of the box. Just check my answer.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.