I am implementing a hierarchy of classes in Python. The common base class exposes a few methods like an interface, there are a few abstract classes that should not be instantiated directly and each of the concrete subclasses can mixin a few of the abstract classes and provide additional behaviour. For example, the following code is a simple example of what I started from:
class BaseEntity(ABC):
@property
@abstractmethod
def f(self) -> List[str]:
pass
@property
@abstractmethod
def g(self) -> int:
pass
class AbstractEntity1(BaseEntity, ABC): # this should not be instantiable
@property
@abstractmethod
def f(self) -> List[str]:
return ["a", "b"]
@property
@abstractmethod
def g(self) -> int:
return 10
class AbstractEntity2(BaseEntity, ABC): # this should not be instantiable
@property
@abstractmethod
def f(self) -> List[str]:
return ["x"]
@property
@abstractmethod
def g(self) -> int:
return 3
class FinalEntity(AbstractEntity1, AbstractEntity2, BaseEntity):
@property
def f(self) -> List[str]:
return ["C"]
@property
def g(self) -> int:
return 10
I would like FinalEntity and all other concrete entities to behave as follows: when I call final_entity.f(), it should return ["a", "b", "x", "C"] (so the equivalent of calling the + operator on each of the mixins and the class itself); similarly, when I call final_entity.g(), it should return 10 + 3 + 10 (i.e. calling the + operator on each of the mixins and the class itself). The functions are obviously just an example and it won’t always be +, so it will have to be defined for each of the functions.
What is the best pythonic way to approach this problem?
>Solution :
Congratulations! You’ve discovered when not to use inheritance. Inheritance is a very specific tool in a programmer’s toolbelt that solves a very specific problem, but there are lots of poor tutorials and poor instructors out there that try to hammer every nail in the software development world with subclasses.
What you have is called a list, not a series of superclasses. If you want FinalEntity to have a bunch of traits, then that’s a has-a relationship. We model has-a relationships with composition, not inheritance.
That is, rather than having FinalEntity inherit from all of its traits, we have it contain them.
class Trait1:
def f(self):
return ["a", "b"]
class Trait2:
def f(self):
return ["x"]
class Trait3:
def f(self):
return ["C"]
Then your FinalEntity can simply be
class FinalEntity:
def __init__(self, traits):
self.traits = traits
def f(self):
return [value for trait in self.traits for value in trait.f()]
As a bonus, this is way easier to write tests for. Your proposed FinalEntity is tightly coupled to its parents, so you wouldn’t be able to test it with different traits without mocking. But this FinalEntity has a constructor that directly plugs-and-plays different traits for free.
Note that, if you really want to use inheritance, you just need to call super in the subclasses, and Python’s default method resolution order will take it the rest of the way.
class BaseEntity:
def f(self):
return []
class AbstractEntity1(BaseEntity):
def f(self):
return super.f() + ["a", "b"]
class AbstractEntity2(BaseEntity):
@abstractmethod
def f(self):
return super.f() + ["x"]
class FinalEntity(AbstractEntity1, AbstractEntity2):
def f(self):
return super.f() + ["C"]
But, again, this is going to result in more brittle code that’s harder to test.
For what it’s worth, the feature you’re looking for is called method combinations. This isn’t a feature available in Python, but some languages, most notably Common Lisp, do support this out of the box. The equivalent to your code in Common Lisp would be
(defclass base () ())
(defclass abstract1 (base) ())
(defclass abstract2 (base) ())
(defclass final (abstract1 abstract2) ())
(defgeneric f (value)
(:method-combination append :most-specific-last))
(defmethod f append ((value abstract1))
(list "a" "b"))
(defmethod f append ((value abstract2))
(list "x"))
(defmethod f append ((value final))
(list "C"))
(let ((instance (make-instance 'final)))
(format t "~A~%" (f instance))) ;; Prints ("a" "b" "x" "C")
But, again, that’s not available in Python without a lot of clever reflection tricks.