Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Stackable traits in Python

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?

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

>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.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading