Python3 generic inheritance hierarchy

Viewed 225

This question is specific for python 3. Suppose I have a class hierarchy like this

class Base():
   def calculate():
       return 0

class Derived1(Base):
     def calculate():
        # some calculation

class Derived2(Base):
     def calculate():
        # some calculation

Now, what I want to do is make a class that defines a generic way to inherit from the Derived classes, and then overrides calculate. In other words, something in the spirit of C++ templates, to avoid copying over the subclasses code, but specify a generic way of subclassing, and then be able to define the subclasses as one liners, like shown below:

# pseudocode
class GenericDerived5(someGenericBase):
      def calculate():
          return super().calculate() + 5

class GenericDerived6(someGenericBase):
      def calculate():
          return super().calculate() + 5

class Derived5_1 = GenericDerived5(Derived1)
class Derived6_1 = GenericDerived6(Derived2)

(the calculation is not literally like this, just illustrating the combinatorial nature of the inheritance structure) How would this code look like, and what are the relevant tools from python3 that I need? I've heard of metaclasses, but not very familiar.

2 Answers

class definition inside a factory-function body

The most straightforward way to go there is really straightforward - but can feel a bit awkward:

def derived_5_factory(Base):
    class GenericDerived5(Base):
        def calculate(self):
            return super().calculate() + 5
    return GenericDerived5

def derived_6_factory(Base):
    class GenericDerived6(Base):
        def calculate(self):
            return super().calculate() + 6
    return GenericDerived6

Derived5_1 = derived_5_factory(Derived1)
Derived6_2 = derived_6_factory(Derived2)

The inconvenient part is that your classes that need generic bases have to be defined inside function bodies. That way, Python re-executes the class statement itself, with a different Base, taking advantage that in Python classes are first class objects.

This code have the inconveniences that (1) the class bodies must be inside functions, and (2) it can be the wrong approach at all:

Multiple inheritance

If you can have an extra inheritance level - that is the only difference for your example, this is the "correct" way to go. Actually, apart from having the former "GenericDerived" classes explicitly in their inheritance chain, they will behave exactly as intended:


class Base():
   def calculate():
       return 0

class Derived1(Base):
     def calculate(self):
        return 1

class Derived2(Base):
     def calculate(self):
        return 2

#  mix-in bases:

class MixinDerived5(Base):
    def calculate(self):
        return super().calculate() + 5

class MixinDerived6(Base):
    def calculate(self):
        return super().calculate() + 6


Derived5_1 = type("Derived5_1", (MixinDerived5, Derived1), {})
Derived6_2 = type("Derived6_2", (MixinDerived6, Derived2), {})

Here, instead of using the class statement, a dynamic class is created with the type call, using both the class that needs a dybamic base and that dynamic base as its bases parameter. That is it - Derived5_1 is a fully working Python class with both Bases in its inheritance chain

Note that Python's super() will do exactly what common sense would expect it to do, "rerouting" itself through the extra intermediary "derived" classes before reaching "Base". So, this is what I get on the interactive console after pasting the code above:

In [6]: Derived5_1().calculate()                                                                 
Out[6]: 6

In [7]: Derived6_2().calculate()                                                                 
Out[7]: 8

A mix-in class, roughly speaking, is a class that isn't intended to be instantiated directly or act as a standalone base class (other than for other, more specialized mix-in classes), but to provide a small subset of functionality that another class can inherit.

In this case, your GenericDerived classes are perfect examples of mix-ins: you aren't creating instances of GenericDerived, but you can inherit from them to add a calculate method to your own class.

class Calculator:
    def calculate(self):
        return 9

class Calculator1(Calculator):
      def calculate(self):
          return super().calculate() + 5

class Calculator2(Calculator):
      def calculate(self):
          return super().calculate() + 10

class Base(Calculator):
    ...

Note that the Base and Calculator hierarchies are independent of each other. Base provides, in addition to whatever else it does, basic calculate functionality. A subclass of Base can use calculate that it inherits from Base (via Calculator), or it can inherit from a subclass of Calculator as well.

class Derived1(Base):
    ...

class Derived2(Base, Calculator1):
    ...

class Derived3(Base, Calculator2):
    ...
Related