0

I would like to write a class that is basically an add-on to existing classes. It should be designed to be only inherited and not work on its own. So far I am using something like this example.

class Swordsman: # First self sufficient class
    def __init__(self):
        pass

    def walk(self):
        pass

    def cover(self):
        print('Covering behind my shield')

class Wizard: # Second self sufficient class
    def __init__(self):
        pass

    def walk(self):
        pass

    def cover(self):
        print('Covering behind my robe')

class HealerAddon: # addon class
    def __init__(self):
        pass

    def heal(self):
        self.cover() # this is ugly
        print('healing')

class Paladin(Swordsman,HealerAddon):
    def __init__(self):
        Swordsman.__init__(self)
        HealerAddon.__init__(self)

class Druid(Wizard,HealerAddon):
    def __init__(self):
        Wizard.__init__(self)
        HealerAddon.__init__(self)

if __name__ == '__main__':
    arthos = Paladin()
    arthos.heal()

    druid1 = Druid()
    druid1.walk()
    druid1.heal()

In the example Swordsman and Wizard are two self suffient classes. HealerAddon is an Addon class that can be used to add healing capability to either of the former two. The result are the two classes Paladin and Druid, which are a healing swordsman and wizard respectiveley.

So far this works but I am not really happy with this way of doing it especially because of the first line in the heal method, where the existance of a cover method is assumed although it becomes only clear in the combined classes Druid and Paladin, where cover is coming from.

Is there a better way of doing this?

Jannick
  • 1,036
  • 7
  • 16
  • FYI: What you call "addon" is usually known as "[mixin](https://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful)". – Aran-Fey Jun 08 '18 at 20:15
  • What is the `__int__` method supposed to do? Why do you want to be able to convert wizards and swordsmen into numbers, and why do you want to override that conversion with a mixin? – abarnert Jun 08 '18 at 20:18
  • 1
    Also, if you have a cooperative multiple-inheritance hierarchy like this, you should look into using `super()` to call the same method in the next class up the chain, instead of calling them all explicitly from the leaf classes. – abarnert Jun 08 '18 at 20:19
  • @abarnert Sry that was a type, it should be init – Jannick Jun 08 '18 at 20:20
  • 1
    I have to admit I don't really see the problem with assuming that a `cover` method exists. Your mixin is intended to be used with classes like these; so the existence of a `cover` method is really a given, and not an assumption. – Aran-Fey Jun 08 '18 at 20:20
  • Maybe consider [**`Strategy`**](https://sourcemaking.com/design_patterns/strategy) and [**`Decorator`**](https://sourcemaking.com/design_patterns/decorator) patterns – Peter Wood Jun 08 '18 at 20:23
  • @abarnert Concering super(). I tried that in my actual code, where I had some problems that certain methods were missing then. Here in this example it works however fine. Could it be that it had to do with the fact that in my actual code the mixin also overwrote some methode? – Jannick Jun 08 '18 at 20:25
  • You may find [this answer](https://stackoverflow.com/questions/9575409/calling-parent-class-init-with-multiple-inheritance-whats-the-right-way/50465583#50465583) I wrote about the use of `super` useful. – Aran-Fey Jun 08 '18 at 20:30
  • 1
    If you really want to document the requirement for `cover`, there are ways to do it—e.g., you can create an ABC with an abstract method (and maybe a subclass hook) and have the mixins check that they're being mixed in with a class that defines it (or write your own simple metaclass that extends ABCMeta so you don't need to repeat it in each mixin), but… that's a lot of work for something that probably doesn't need any more than `# Mixin designed to be used with base character classes (walk, cover, etc.)`. – abarnert Jun 08 '18 at 20:48

1 Answers1

3

As the comments have said: what you want is known as a "mixin" class.

The usual naming convention for such a class is for it to end in "mixin", like so:

class HealerMixin:
    def __init__(self, *args, **kwargs):
        if type(self) is HealerMixin:
            raise TypeError("HealerMixin cannot be instantiated")
        super().__init__(*args, **kwargs)
    def heal(self, target=None):
        if target is None:
            # heal self
            target = self.name
        self.cover() # this is ugly
        print('healing {!s}'.format(target))

class NameMixin:
    def __init__(self, name, *args, **kwargs):
        if type(self) is NameMixin:
            raise TypeError("NameMixin cannot be instantiated")
        self.name = str(name)
        super().__init__(*args, **kwargs)
    def __str__(self):
        return self.name

If you include an __init__ method in your mixin, it's usually a good idea to include the (*args, **kwargs) signature in the __init__ method because this enables the mixin to work with any child class with any signature.

class Wizard(NameMixin):
    def cover(self):
        print('{!s} Covering behind my robe'.format(self.name))

class Druid(HealerMixin, Wizard):
    pass

class FlashyDruid(Druid):
    def __init__(self, name):
        super().__init__("~~***{!s}***~~".format(name))
    def cover(self):
        """Overrides Wizard.cover()"""
        print('{!s} Covering behind my glittering robe'.format(self.name))

Maybe use it like this:

>>> Gob = FlashyDruid("Gob")
>>> Gob.cover()
~~***Gob***~~ Covering behind my glittering robe
>>> Buster = Druid("Buster")
>>> Buster.heal(Gob)
Buster Covering behind my robe
healing ~~***Gob***~~

Using super in your __init__ method makes it so that the class inheritance happens auto-magically. You do not have to manually think through the inheritance yourself.

Note that it sometimes becomes important to inherit from a mixin class in the right order: mixins first, the base classes last (if there are any). If you do not, you will cause errors. For example in some cases the mixin will try to send arguments to classes that do not know what to do with them.

Rick
  • 43,029
  • 15
  • 76
  • 119