Search code examples
pythonpython-3.xoopdiamond-problem

Handling diamond inheritance super class invocations in Python


I have a class setup that looks like below

from abc import abstractmethod


class Player:
    def __init__(self, name, age):
        self._player_name = name
        self._age = age

    @property
    def player_name(self):
        return self._player_name

    @property
    def age(self):
        return self._age

    @abstractmethod
    def _prefix(self) -> str:
        pass

    @abstractmethod
    def _suffix(self) -> str:
        pass

    def pretty_print(self) -> str:
        return f"{self.player_name} is a {self._prefix()} and is {self.age} years old. Accomplishments: {self._suffix()}"


class Footballer(Player):
    def __init__(self, name, age, goals_scored):
        super().__init__(name, age)
        self._goals_scored = goals_scored

    @property
    def goals_scored(self):
        return self._goals_scored

    def _prefix(self) -> str:
        return "Football Player"

    def _suffix(self) -> str:
        return f"Goals Scored {self._goals_scored}"


class CarRacer(Player):
    def __init__(self, name, age, races_won, laps):
        super().__init__(name, age)
        self._races_won = races_won
        self._laps = laps

    @property
    def laps(self):
        return self._laps

    @property
    def races_won(self):
        return self._races_won

    def _prefix(self) -> str:
        return "Formula 1 racer"

    def _suffix(self) -> str:
        return f"Races won: {self.races_won}, Laps count: {self.laps}"


class AllRounder(Footballer, CarRacer):
    def __init__(self, name, age, goals_scored, races_won, laps):
        super().__init__(name, age, goals_scored)
        super(CarRacer, self).__init__(name, age, races_won, laps)

    def _prefix(self) -> str:
        return "All Rounder"

    def _suffix(self) -> str:
        return f"{Footballer._prefix(self)}, {CarRacer._prefix(self)}"

Now within my main method, I am doing the following:

if __name__ == '__main__':
    all_rounder = AllRounder("Jack", 30, 150, 200, 1000)
    print(all_rounder.pretty_print())

When the instantiation kicks in, I keep hitting the below error

Traceback (most recent call last):
  File "/Users/kmahadevan/githome/playground/python_projects/playground/pythonProject/oops/diamond_inheritance.py", line 79, in <module>
    all_rounder = AllRounder("Jack", 30, 150, 200, 1000)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kmahadevan/githome/playground/python_projects/playground/pythonProject/oops/diamond_inheritance.py", line 68, in __init__
    super().__init__(name, age, goals_scored)
  File "/Users/kmahadevan/githome/playground/python_projects/playground/pythonProject/oops/diamond_inheritance.py", line 31, in __init__
    super().__init__(name, age)
TypeError: CarRacer.__init__() missing 2 required positional arguments: 'races_won' and 'laps'

I am not entirely sure as to how should the __init__ method within AllRounder be called so that both the base classes are invoked.

A pictorial representation of the class hierarchy for a bit more better understanding

enter image description here


Solution

  • When a class inherits from multiple bases, Python linearises the inheritance tree. This linear list of classes can be accessed by looking up <className>.__mro__ on any class.

    print(AllRounder.__mro__) will have this output:

    (<class '__main__.AllRounder'>, <class '__main__.Footballer'>, <class '__main__.CarRacer'>, <class '__main__.Player'>, <class 'object'>)
    

    Calling super() on a class will result in Python looking up the next class in the above list, and calling its __init__. In this case, when Footballer calls super(), CarRacer.__init__ gets called; not Player.__init__, though Footballer's class definition says: class Footballer(Player)

    One way to solve this is by having all __init__ methods consume a **kwargs. A class's __init__ could declare only those args which it actually consumes, while letting all other args get passed up the chain.

    If the **kwargs have a key which matches a positional argument name, then python substitutes the argument's value and removes that key from kwargs.

    Unrelated Note: For Player to be an abstract base class, it must derive from abc.ABC, such that it errors if Player is instantiated as it as abstract methods.


    Code:

    from abc import ABC, abstractmethod
    
    
    class Player(ABC):
        def __init__(self, name, age, **kwargs):
            super().__init__(**kwargs)
            self._player_name = name
            self._age = age
    
        @property
        def player_name(self):
            return self._player_name
    
        @property
        def age(self):
            return self._age
    
        @abstractmethod
        def _prefix(self) -> str:
            pass
    
        @abstractmethod
        def _suffix(self) -> str:
            pass
    
        def pretty_print(self) -> str:
            return f"{self.player_name} is a {self._prefix()} and is {self.age} years old. Accomplishments: {self._suffix()}"
    
    
    class Footballer(Player):
        def __init__(self, goals_scored, **kwargs):
            super().__init__(**kwargs)
            self._goals_scored = goals_scored
    
        @property
        def goals_scored(self):
            return self._goals_scored
    
        def _prefix(self) -> str:
            return "Football Player"
    
        def _suffix(self) -> str:
            return f"Goals Scored {self._goals_scored}"
    
    
    class CarRacer(Player):
        def __init__(self, races_won, laps, **kwargs):
            super().__init__(**kwargs)
            self._races_won = races_won
            self._laps = laps
    
        @property
        def laps(self):
            return self._laps
    
        @property
        def races_won(self):
            return self._races_won
    
        def _prefix(self) -> str:
            return "Formula 1 racer"
    
        def _suffix(self) -> str:
            return f"Races won: {self.races_won}, Laps count: {self.laps}"
    
    
    class AllRounder(Footballer, CarRacer):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
    
        def _prefix(self) -> str:
            return "All Rounder"
    
        def _suffix(self) -> str:
            return f"{Footballer._prefix(self)}, {CarRacer._prefix(self)}"
    
    
    if __name__ == "__main__":
    
        args = {
            "name": "John Doe",
            "age": 41,
            "goals_scored": 1,
            "races_won": 2,
            "laps": 500
        }
    
        a = AllRounder(**args)
        print(a.pretty_print())
    
        print("\n== MRO ==")
        print(AllRounder.__mro__)
    
    

    The output would be:

    John Doe is a All Rounder and is 41 years old. Accomplishments: Football Player, Formula 1 racer
    
    == MRO ==
    (<class '__main__.AllRounder'>, <class '__main__.Footballer'>, <class '__main__.CarRacer'>, <class '__main__.Player'>, <class 'abc.ABC'>, <class 'object'>)