Problematic diamond inheritance in Python

Question:

Let there be an abstract class Animal which is extended and implemented by class Dog and class Cat.

class Animal:
  def speak(self):
    raise NotImplementedError

  ... # 50 other methods with implementation and another 40 without implementation (abstract, headers only)

class Dog(Animal):
  def speak(self):
    return "ruff"

  ... # 40 more implemented methods

class Cat(Animal):
  def speak(self):
    return "purr"

  ... # 40 more implemented methods

There is also a bunch of functionality that can turn Animals into RoboAnimals, which causes every Animal type to say "beep boop" before anything whenever they speak.

How should one implement both a class RoboDog and a class RoboCat while avoiding duplicating code? Neither Dog, Cat, or Animal can be manipulated.

I’ve tried using another classRoboAnimal — that extends Animal in order to cover for 50 basic Animal methods, but Animal has 40 other abstract methods that need appropriate implementation (Dog/Cat). If I use composition, and simply wrap a Dog or Cat instance, I’d still have to make sure that all Animal methods called on this object behave accordingly to whether the flying animal is actually a Dog or a Cat.

Just to be clear, a RoboDog returns "beep boop ruff" on .speak() calls and otherwise behaves like a Dog, and likewise with Cat ("beep boop purr").

Can you do that in Python 3.10?

EDIT:
My attempts at solving the problem:

#1

class RoboDog(Dog):
  def speak(self):
    return "beep boop " + super().speak()

class RoboCat(Cat):
  def speak(self):
    return "beep boop " + super().speak()

  # damn, I've just duplicated the code...
  # what if adding robotic functionality 
  # to animals took hundreds of lines of 
  # code, now that would be a hassle to maintain

#2

class RoboAnimal(Animal):
  def speak(self):
    return "beep boop " + super().speak()
    # oh snap, super().speak() raises an error :(

#3

class RoboAnimal(Animal):
  def __init__(self, animal_cls, ...):
    super().__init__(...)
    self.wrapped_animal = animal_cls(...)

  def speak(self):
    return "beep boop " + self.wrapped_animal.speak()
    # now this is epic, but none of the 40 abstract methods work now
Asked By: Captain Trojan

||

Answers:

You can create a class that overrides only one of the methods you need, and insert it in the right order in the inheritance tree. This is called a mixin:

class Animal:
    pass

class Dog(Animal):
    def speak(self):
        return 'woof!'

class RoboAnimal:
    def speak(self):
        return 'beep boop ' + super().speak()

class RoboDog(RoboAnimal, Dog):
     pass

print(RoboDog().speak())

Notice that RoboDog.speak calls super().speak. This means that if you don’t put a valid instance of Animal in the inheritance chain after it, it will crash if it is called. RoboAnimal does not need to inherit from Animal because it serves an entirely different purpose. In fact I would probably call the class something like RoboMixin to avoid confusion.

In general, the order of base classes in multiple inheritance is important. For example, in the following case, the mixin method would never be called because Dog.speak never calls super().speak:

class WrongRoboDog(Dog, RoboAnimal):
    pass
Answered By: Mad Physicist
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.