Search code examples
pythonmultiple-inheritancediamond-problem

Diamond inheritance in Python with different signatures


Here's the setup:

class Player(object):
    def __init__(self, heigth):
        self.heigth = heigth
        print('do not forget that this should happen once!')

class Attacker(Player):
    def __init__(self, heigth, goal_probability):
        super().__init__(heigth)
        self.goal_prob = goal_probability

    def hit(self):
        pass
        # implementation

class Goalie(Player):
    def __init__(self, heigth, save_probability=0.1):
        super().__init__(heigth)
        self.save_prob = save_probability

    def catch(self):
        pass
        # implementation

class UniversalPlayer(Attacker, Goalie):
    pass

up = UniversalPlayer(heigth=1.96, goal_probability=0.6)

It all works as expected: the MRO chooses Attacker first, then Goalie. I call UniversalPlayer's constructor with Attacker's __init__ signature, Goalie's constructor is called with Player's signature, it goes ok because save_probability has a default value but the problem is that I have no way of choosing save_probability, apart from setting up.save_probability after instantiating up, which I find very inelegant.

Furthermore, had Goalie not had a default value for save_probability, this code would raise an exception.

Is there a way to write UniversalPlayer so that I can choose save_probability too, or is there some fundamental problem here that cannot be worked around?


Solution

  • Each additional parameter to __init__ needs to have a class responsible for removing it from calls to super, so that when object.__init__ is finally called, you don't accidentally pass any arguments to it. Additionally, each method has to accept arbitrary arguments and pass them on for the next method to possibly handle.

    # Player will be responsible for height
    class Player(object):
        def __init__(self, height, **kwargs):
            super().__init__(**kwargs)  # Player needs to use super too!
            self.height = height
            print('do not forget that this should happen once!')
    
    
    # Attacker will be responsible for goal_probability
    class Attacker(Player):
        def __init__(self, height, goal_probability, **kwargs):
            super().__init__(height, **kwargs)
            self.goal_prob = goal_probability
    
        def hit(self):
            pass
    
    
    # Goalie will be responsible for save_probability
    class Goalie(Player):
        def __init__(self, height, save_probability=0.1, **kwargs):
            super().__init__(height, **kwargs)
            self.save_prob = save_probability
    
        def catch(self):
            pass
            # implementation
    
    class UniversalPlayer(Attacker, Goalie):
        pass
    
    # Pass all arguments
    # Life is easier if you stick to keyword arguments when using super().__init__
    up = UniversalPlayer(height=1.96, goal_probability=0.6, save_probability=0.2)
    

    Now, Attacker.__init__ is the first to be called. It uses goal_probability, then does not pass it on to other calls. It accepts save_probability via **kwargs and passes it on for Goalie.__init__ to eventually receive. Note that neither Attacker.__init__ nor Goalie.__init__ would have to explicitly include height in their argument lists; it could also be accepted via **kwargs to be eventually received by Player.__init__.