Hernán Erasmo
Hernán Erasmo

Reputation: 371

Correct way of passing a self variable as argument to a mixin parent method

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?

Upvotes: 1

Views: 1628

Answers (1)

chepner
chepner

Reputation: 531490

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.

Upvotes: 3

Related Questions