Lester Jack
Lester Jack

Reputation: 199

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?

Upvotes: 1

Views: 253

Answers (2)

PyPingu
PyPingu

Reputation: 1747

Besides the fact I'm not sure if separate classes is the best way to handle these, the issue is that your constructors can't handle unknown arguments. To allow them to use the *args, **kwargs notation. Effectively all arguments will be passed to each __init__ and the unused ones ignored.

class Player(object):
    def __init__(self, *args, **kwargs):
        self.height = kwargs['height']

class Attacker(Player):
    def __init__(self, goal_probability, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.goal_prob = goal_probability

    def hit(self):
        pass
        # implementation

class Goalie(Player):
    def __init__(self, save_probability, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.save_prob = save_probability

    def catch(self):
        pass
        # implementation

class UniversalPlayer(Attacker, Goalie):
    pass

up = UniversalPlayer(height=1.96, goal_probability=0.6, save_probability=0.2)

Upvotes: 0

chepner
chepner

Reputation: 531345

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

Upvotes: 3

Related Questions