4

The actual code is vastly different and on a whole different topic, but I felt that this small example might be better since my problem is understanding the key concepts for complex inheritance scenarios (and not my specific domain).

Let's consider we have a basic Entity class:

from enum import Enum
from abc import abstractmethod

class Condition(Enum):
    ALIVE = 1
    DEAD = 2
    UNDEAD = 3

class Entity(object):

    def __init__(self):
        self.condition = Condition.ALIVE
        self.position = 0
        self.hitpoints = 100

    def move(self):
        self.position += 1

    def changeHitpoints(self, amount):
        self.hitpoints += amount

    @abstractmethod
    def attack(self, otherEntity):
        pass

This is a base class other concrete entities inherit from and attack() has to be abstract, since every entity should implement its own style of attack method.

Now we could implement some entities:

class Orc(Entity):

    def __init__(self):
        super().__init__()
        self.hitpoints = 150
        self.damage = 10

    def attack(self, otherEntity : Entity):
        otherEntity.changeHitpoints(-self.damage)

class Human(Entity):

    def __init__(self):
        super().__init__()
        self.damage = 8

    def attack(self, otherEntity : Entity):
        otherEntity.changeHitpoints(-self.damage)

class Undead(Entity):

    def __init__(self):
        super().__init__()
        self.condition = Condition.UNDEAD
        self.damage = 5

    def attack(self, otherEntity : Entity):
        # harm enemy
        otherEntity.changeHitpoints(-self.damage)
        # heal yourself
        self.changeHitpoints(1)

This works fine. However, I am struggling to figure out a good solution (DRY-style) for implementing "abilities" and other stuff.

For example, if Orc and Human should not only move, but also be able to jump, it would be interesting to have something like:

class CanJump(Entity):

    def jump(self):
        self.position += 2

class Orc(Entity, CanJump):
    (...)

class Human(Entity, CanJump):
    (...)

This introduces two problems. (1) we need access to self.position in CanJump, thus we have to inherit from Entity?! If we do so, we have to implement the abstract method attack() in class CanJump. This does not make sense, since CanJump should just give entities the ability of a new type of movement. (2) in the future we might want to implement for example a decorator that checks if the condition of an entity is Condition.DEAD before executing move(), attack(), ... This also means CanJump needs access to self.condition.

What would be a clean solution for this type of problems?

What if there is a need for further subclassing? E.g. we might be interested in creating an UndeadHuman like class UndeadHuman(Undead, Human). Due to the linearization (Undead first in order) it should have the attack behavior of an Undead but it also needs the CanJump from a Human.

daniel451
  • 10,626
  • 19
  • 67
  • 125

2 Answers2

3

[W]e need access to self.position in CanJump, thus we have to inherit from Entity?!

No, you don't. You can treat CanJump as a mix-in class, that simply adds functionality. Any classes that subclass CanJump are expected to have a position attribute. And the CanJump class itself, doesn't need to inherit from Entity. So doing:

class CanJump:
    def jump(self):
        self.position += 2

Would be perfectly fine. You can then do:

class Orc(Entity, CanJump):
    (...)

class Human(Entity, CanJump):
    (...)

Here is a complete example that demonstrates the above would work:

from abc import abstractmethod


class A:
    def __init__(self):
        self.a = 0 

    @abstractmethod
    def m(self):
        pass


class C:
    def c(self):
        self.a += 1


class B(A, C):
    def __init__(self):
        super().__init__()

    def m(self):
        print('method in B')


b = B()
print(b.a) # 0
b.c()
print(b.a) # 1
b.m() # method in B

You see, you can use currently non-existent attributes in a method implementation. The attributes just need to exist when the method is called. And letting subclasses of CanJump implement the needed attributes works well.

If you want to force users of your class to defined certain attributes, you can use a meta-class. Rather than repeating information, I'll point you to @kindall's answer, which deals with this rather elegantly.

Christian Dean
  • 22,138
  • 7
  • 54
  • 87
  • This is an interesting concept. Is this a Python / duck-typing related feature? I have never seen *mix-in* classes before. Is there a way to prevent `CanJump` from being instantiated directly? One might do this by accident and then `self.position += 2` would have an unresolved reference. Additionally, I am wondering if we could type hint attributes somehow? – daniel451 Sep 07 '17 at 18:52
  • 1
    @daniel451 Not at all, many other languages support mix-in classes. They may be called by different names however, such as traits in PHP. Although it's a bit more sticky to implement in statically-typed languages. I'll update with an example addressing your other concerns in a bit. – Christian Dean Sep 07 '17 at 19:13
  • @daniel451 it's a common technique for languages with multiple inheritance. You can do the same thing in Java now with interfaces that provide default methods. – JAB Sep 07 '17 at 19:14
1

I'd be tempted to model this not via inheritance, but composition.

You can have an Entity class that gets used for every creature and a separate RaceModifier class that sets the relevant constants/adjustments and abilities. Then each creature can have a list of these to model the races in game, for example:

class RaceModifier:
    def __init__(self):
        ...
        self.abilities = []

class Orc(RaceModifier):
    def __init__(self):
        super().__init__()
        self.extra_hit_points = 50
        self.extra_damage = 2
        self.abilities = [JUMPING]

class Undead(RaceModifier):
    def __init__(self):
        super().__init__()
        self.abilities = [REGEN_ON_ATTACK]

class Entity:
    ...
    def has_ability(self, ability): # ability = JUMPING etc
        for race in self.race_modifiers:
            if ability in race.abilities:
                return True
        for eq in self.equipment: # wearing a ring of jumping ? :)
            if ability in eq.abilities:
                return True
        return False
Glenn Rogers
  • 351
  • 2
  • 4
  • This is also an interesting concept. Do you have some more (good) examples for *composition* in Python? – daniel451 Sep 07 '17 at 19:40
  • `abilities` should probably be a `dict`, so that you can more quickly check for an appropriate ability. – chepner Sep 07 '17 at 20:33