Woden
Woden

Reputation: 1386

Multiple inheritance with super() in Python

I am practicing multiple inheritances in Python. Without the Boss class, everything goes well. Any help is highly appreciated. I've referred to :How does Python's super() work with multiple inheritance?

The feedback:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in 
 41 print(archer1.__str__())
 42 print('')
---> 43 boss = Boss("Boss", 50, 50, 100)
 44 print(boss.__str__())

 in __init__(self, name, power, agility, HP)
 27 class Boss(Worrior,Archer):
 28      def __init__(self, name, power, agility, HP):
---> 29      Worrior.__init__(self, name, power, HP)
 30          Archer.__init__(self, name, agility, HP)
 31      def __str__(self):

 in __init__(self, name, power, HP)
  7 class Worrior(Player):
  8     def __init__(self, name, power, HP):
----> 9     super().__init__(HP)
 10         self.name = name
 11         self.power = power

TypeError: __init__() missing 2 required positional arguments: 'agility' and 'HP'

It seems after taking the power attribute inside the Worrior class, and then stop.

class Player:
def __init__(self,HP):
    self.HP = HP 
def sign_in(self):
    print('player sign in')
# put the class want to extend from

class Worrior(Player):
def __init__(self, name, power, HP):
    super().__init__(HP)
    self.name = name
    self.power = power
# it's the toString() method in java
# need to override the dunder(magic) method
def __str__(self):
    return "The worrior's name: " f'{self.name} \n' \
        "He has the power:" f'{self.power}'

class Archer(Player):
def __init__(self, name, agility, HP):
    super().__init__(HP)
    self.name = name
    self.agility = agility
def __str__(self):
    return "The archer's name: " f'{self.name} \n' \
        "He has the agility:" f'{self.agility}'

class Boss(Worrior,Archer):
def __init__(self, name, power, agility, HP):
     Worrior.__init__(self, name, power, HP)
     Archer.__init__(self, name, agility, HP)
def __str__(self):
    return "The boss's name: " f'{self.name} \n' \
        "With Worrior's power " f'{self.power} \n' \
        "and With Archer's agilit" f'{self.agility}'\
        "The boss' HP is: " f'{self.HP}'
boss = Boss("Boss", 50, 50, 100)
print(boss.__str__())

Upvotes: 2

Views: 2956

Answers (3)

Tabaene Haque
Tabaene Haque

Reputation: 574

This is happening because of the method resolution order (MRO) in new style classes. MRO of the Boss class -

ipdb> Boss.mro()                                                                                                                   
[<class '__main__.Boss'>, <class '__main__.Worrior'>, <class '__main__.Archer'>, <class '__main__.Player'>, <class 'object'>]
ipdb>  

This means that super() calls in Worrior class is actually referring to Archer, not Player.

You can refer to this stack overflow post - Method Resolution Order (MRO) in new-style classes?, which explains nicely how MRO works in python.

Upvotes: 1

Dane White
Dane White

Reputation: 3562

The link from @Thierry Lathuille is the correct one to read, but I'll try to add some extra explanation. The MRO for the initializer is [Boss, Worrior, Archer, Player]. What this means (somewhat confusingly) is that when Worrior calls super(), this is actually referring to Archer, not Player. If you place print(super().__init__) before each time you call the method, you will find output like this prior to your crash:

<bound method Worrior.__init__ of <__main__.Boss object at 0x10af5f780>>
<bound method Archer.__init__ of <__main__.Boss object at 0x10af5f780>>
Traceback (most recent call last):
...

This is the major pitfall IMHO with multiple inheritance in Python, and I generally advise against it unless you have zero-argument initializers.

If you try to explicitly call each base class initializer (e.g. Player.__init__), then you will end up with some of your initializers executing multiple times.

In order to be able to pass your arguments through, you'll need to leverage **kwargs. Additionally, as these get passed through, each unique argument name will get stripped from the kwargs. So this means that you can't reuse name as an initialization argument. I'll use worrior_name and archer_name instead. So putting this all together, the following will run each initializer only once, without crashing:

class Player:
    def __init__(self, hp, **kwargs):
        print(super().__init__)
        self.hp = hp

    def sign_in(self):
        print('player sign in')


class Worrior(Player):
    def __init__(self, worrior_name, power, **kwargs):
        super().__init__(**kwargs)
        self.name = worrior_name
        self.power = power

    def __str__(self):
        return "The worrior's name: " f'{self.name} \n' \
               "He has the power:" f'{self.power}'


class Archer(Player):
    def __init__(self, archer_name, agility, **kwargs):
        super().__init__(**kwargs)
        self.name = archer_name
        self.agility = agility

    def __str__(self):
        return "The archer's name: " f'{self.name} \n' \
               "He has the agility:" f'{self.agility}'


class Boss(Worrior, Archer):
    def __init__(self, name, power, agility, hp):
        super().__init__(archer_name=name, worrior_name=name, power=power, agility=agility, hp=hp)

    def __str__(self):
        return "The boss's name: " f'{self.name} \n' \
               "With Worrior's power " f'{self.power} \n' \
               "and With Archer's agilit" f'{self.agility}' \
               "The boss' hp is: " f'{self.hp}'

Upvotes: 3

Grismar
Grismar

Reputation: 31461

It appears that Python decides to walk the entire hierarchy again when it arrives at super() in the Warrior class (I've corrected your spelling, it's not 'Worrior').

I'm not sure where you're going wrong or whether this is just a situation where you cannot use super(). I expected the same as you and was surprised by the result as well.

However, the code below doesn't have the problem, although you of course lose the flexibility and I don't like having to do this:

class Player:
    def __init__(self, hp):
        self.hp = hp

    def sign_in(self):
        print('player sign in')


class Warrior(Player):
    def __init__(self, name, power, hp):
        Player.__init__(self, hp)
        self.name = name
        self.power = power

    def __str__(self):
        return "The warrior's name: " f'{self.name} \n' \
            "He has the power:" f'{self.power}'


class Archer(Player):
    def __init__(self, name, agility, hp):
        Player.__init__(self, hp)
        self.name = name
        self.agility = agility

    def __str__(self):
        return "The archer's name: " f'{self.name} \n' \
            "He has the agility:" f'{self.agility}'


class Boss(Warrior, Archer):
    def __init__(self, name, power, agility, hp):
        Warrior.__init__(self, name, power, hp)
        Archer.__init__(self, name, agility, hp)

    def __str__(self):
        return "The boss's name: " f'{self.name} \n' \
            "With Warrior's power: " f'{self.power} \n' \
            "and With Archer's agility: " f'{self.agility}'\
            "The boss' HP is: " f'{self.hp}'


boss = Boss("Boss", 50, 50, 100)
print(boss.__str__())

So, not really a complete answer, but wanted to provide the feedback - hope someone else provides a complete answer with an explanation of what is really going on.

Upvotes: 0

Related Questions