1

I have to model a warrior and the different kinds of attacks he can perform. The idea is to use mixins to contain the attack logic. I have my classes defined in the following way:

class Warrior: def __init__(self, energy): self.energy = energy class TemplarKnight(Warrior, HandToHandCombatMixin): pass class CombatMixin: def __init__(self): self.attacks_cost = {} def attack(self, attacker, attack_cost): if attacker.energy < attack_cost: print('Not enough energy to attack') else: attacker.energy -= attack_cost print('Attack!') class HandToHandCombatMixin(CombatMixin): def __init__(self): super().__init__() self.attacks_cost['sword_spin'] = 10 def sword_spin(self, attacker): return self.attack(attacker, self.attacks_cost['sword_spin']) 

But the problem comes when I try to test this setup. When I do

class TestTemplarKnight(unittest.TestCase): def setUp(self): self.templar = TemplarKnight(energy=100) def test_templar_knight_can_sword_spin(self): self.templar.sword_spin(self.warrior) self.assertEquals(self.templar.energy, 90) 

I get

 def sword_spin(self, attacker): return self.attack( > attacker, self.attacks_cost['sword_spin']) E AttributeError: 'TemplarKnight' object has no attribute 'attacks_cost' 

It seems that Python thinks that the parameter self.attacks_cost (when calling self.attack() inside the sword_spin() method of the HandToHandCombatMixin class) belongs to the TemplarKnight class instead of the HandToHandCombatMixin.

How should I have written this code to make Python look for self.attacks_cost inside HandToHandCombatMixin?

2
  • All the __init__ methods in this case should be using super(). Commented Feb 7, 2020 at 14:26
  • Attributes don't really belong to classes; they all belong to the same instance, but each one only gets created if the __init__ method that sets them gets called. Commented Feb 7, 2020 at 14:34

1 Answer 1

3

To use super correctly, all the classes involved need to use it. Right now, Warrior.__init__ is called first, but it doesn't use super, so HandToHandCombatMixin.__init__ is never called.

Make the following additions:

class Warrior: def __init__(self, energy, **kwargs): super().__init__(**kwargs) self.energy = energy class TemplarKnight(Warrior, HandToHandCombatMixin): pass class CombatMixin: def __init__(self, **kwargs): super().__init__(**kwargs) self.attacks_cost = {} def attack(self, attacker, attack_cost): if attacker.energy < attack_cost: print('Not enough energy to attack') else: attacker.energy -= attack_cost print('Attack!') class HandToHandCombatMixin(CombatMixin): def __init__(self, **kwargs): super().__init__(**kwargs) self.attacks_cost['sword_spin'] = 10 def sword_spin(self, attacker): return self.attack(attacker, self.attacks_cost['sword_spin'])

Now when you instantiate TemplarKnight, you'll guarantee that all the __init__ methods are called, and in the correct order. Eventually, once of the calls to super() will cause object.__init__ to be called, at which point the chain finally ends. If you are correctly handling the keyword arguments, **kwargs will be empty by the time that happens.

Sign up to request clarification or add additional context in comments.

2 Comments

The only change that was needed was the addition of super().__init__() at the beginning of the Warrior class. It made no difference in the output of the test to add the same call at the beginning of CombatMixin. Why would that be?
At least in this case, CombatMixin is the last class (before object) in the MRO of TemplarKnight, and object.__init__ doesn't really do anything, so it doesn't matter if it doesn't get called. But, there could be some other class that CombatMixin doesn't know about that could have an MRO with CombatMixin in a different position, so you still need to use super to avoid breaking the chain.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.