Reputation: 119
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?
Upvotes: 0
Views: 58
Reputation: 110591
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.
Upvotes: 1
Reputation: 8086
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.
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
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()
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()
b_a:
barB
fooA
b_d:
barB
fooD
c_d:
barC
fooD
Upvotes: 2