2

I'm facing the following problem:

  • There's a base class Unit, which has a couple of attributes, e.g. id, type, name, skills, ...
  • There are different types of units, some of them have additional attributes like health, attack, or tribe, so naturally the relevant subclasses HealthUnit, AttackUnit, etc. exist as well.
  • There are a few units that have multiple of these attributes, e.g. HealthAttackUnit, or HealthAttackTribeUnit.

I want to avoid coding like this:

class Unit(object):
    def __init__(self, id, type, name, skills):
        self.id= id
        self.type= type
        self.name= name
        self.skills= skills

class HealthUnit(Unit):
    def __init__(self, id, type, name, skills, health):
        Unit.__init__(self, id, type, name, skills)
        self.health= health

class AttackUnit(Unit):
    def __init__(self, id, type, name, skills, attack):
        Unit.__init__(self, id, type, name, skills)
        self.attack= attack

class HealthAttackUnit(HealthUnit, AttackUnit):
    def __init__(self, id, type, name, skills, health, attack):
        HealthUnit.__init__(self, id, type, name, skills, health)
        AttackUnit.__init__(self, id, type, name, skills, attack)

for obvious reasons.


I tried to use dict unpacking as a workaround, kind of like this:

class HealthUnit(Unit):
    def __init__(self, health, **args):
        Unit.__init__(self, **args)
        self.health= health

but even this comes with lots of duplicate code:

class HealthAttackUnit(HealthUnit, AttackUnit):
    def __init__(self, health, attack, **args):
        HealhUnit.__init__(self, health=health, **args)
        AttackUnit.__init__(self, attack=attack, **args)

class HealthAttackTribeUnit(HealthUnit, AttackUnit, TribeUnit):
    def __init__(self, health, attack, tribe, **args):
        HealhUnit.__init__(self, health=health, **args)
        AttackUnit.__init__(self, attack=attack, **args)
        TribeUnit.__init__(self, tribe=tribe, **args)

Plus, this will call Unit.__init__ multiple times, which is less than ideal.

So, the question is: Is there a better, less copy/paste, way of doing this?

Update: The dict unpacking is nice and all, but it's still a little annoying having to call all constructors with keyword arguments. I'd prefer a solution without **kwargs, but I'm guessing there probably isn't one?

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • If you use `super()`, you won't have to call each inherited `__init__` separately - see e.g. http://stackoverflow.com/q/576169/3001761. You will need to use `*args, **kwargs`, too. – jonrsharpe Nov 12 '14 at 15:42
  • Unless you instantiate objects of `HealthUnit` and `AttackUnit`, you might want to be interested in [mixins](http://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful) instead. – poke Nov 12 '14 at 15:45
  • @poke those *are* mixins, surely? – Daniel Roseman Nov 12 '14 at 15:49
  • @DanielRoseman Since they inherit from the general `Unit`, instead of *just* providing health or attack stuff, I don’t think so. – poke Nov 12 '14 at 15:51
  • @DanielRoseman: No, they aren't mixins, because need to instantiate `HealthUnit` and `AttackUnit` objects. – Aran-Fey Nov 12 '14 at 15:59
  • there is an alternate approach. Instead of chaining __init__ let init call yet another function for each inherited class. – joojaa Nov 12 '14 at 16:01

1 Answers1

4

Yes, this is exactly why the super function exists.

Ensure all your __init__ files call super, and Python will work out the MRO for you and call the relevant classes in turn.

class HealthUnit(Unit):
    def __init__(self, **kwargs):
        self.health = kwargs.pop('health')
        super(HealthUnit, self).__init__(**kwargs)

class AttackUnit(Unit):
    def __init__(self, **kwargs):
        self.attack = kwargs.pop('attack')
        super(AttackUnit, self).__init__(**kwargs)

class TribeUnit(Unit):
    def __init__(self, **kwargs):
        self.tribe = kwargs.pop('tribe')
        super(TribeUnit, self).__init__(**kwargs) 

class HealthAttackTribeUnit(HealthUnit, AttackUnit, TribeUnit):
    pass

See also Python core contributor (and occasional SO poster) Raymond Hettinger's article Super considered super, but note the syntax in that post is for Python 3, there's a separate link to the version 2 code.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • I'm not sure I would actually *remove* e.g. `kwargs['health']` before calling the superclass, I'd do e.g. `kwargs.get('health')`, which also allows setting a default. +1 otherwise. – jonrsharpe Nov 12 '14 at 15:53
  • 1
    The reason I do that is that higher up the hierarchy the classes may not be expecting those arguments: and if they're not set up to accept `**kwargs` it will cause a TypeError. Certainly that will happen if you get all the way to `object`. Best to remove the arguments when you deal with them, I reckon. – Daniel Roseman Nov 12 '14 at 15:55
  • This is a pretty good solution, but it forces me to call all constructors with keyword arguments. That's not really a problem, but I still want to ask: Is there a way around the dict unpacking? P.S.: I _finally_ understand what `super` does, thank you, and take my upvote, kind sir. – Aran-Fey Nov 12 '14 at 16:13
  • There's not reallly a good way of doing it with args rather than kwargs. The problem is that you'd need to guarantee the MRO, which depends on the inheritance graph. Raymond's article goes into detail about exactly how that is worked out if you're interested, but even he recommends using kwargs over args for something like this. – Daniel Roseman Nov 12 '14 at 16:28
  • I see. Oh well. I'll just add it to the pile of things I don't like about python. – Aran-Fey Nov 12 '14 at 16:46