4

A question about whether or not I'm going about something in the best way...

I would like to have a class hierarchy in Python that looks (minimally) like the following;

class Actor
  class Mover(Actor)
  class Attacker(Actor)
    class Human(Mover, Attacker)

But I run up against the fact that Actor has a certain attribute which I'd like to initialise, from each of the Mover and Attacker subclasses, such as in below;

class Actor:
    _world = None
    def __init__(self, world):
        self._world = world

class Mover(Actor):
    _speed = 0
    def __init__(self, world, speed):
        Actor.__init__(self, world)
        self._speed = speed

class Attacker(Actor):
    _range = 0
    def __init__(self, world, range):
        Actor.__init__(self, world)
        self._range = range

If I was then to go with my initial approach to this, and follow what I always have in terms of using superclass' constructors, I will obviously end up calling the Actor constructor twice - not a problem, but my programmer sense tingles and says I'd rather do it a cleaner way;

class Human(Mover, Attacker):
    def __init__(self, world, speed, range):
        Mover.__init__(self, world, speed)
        Attacker.__init__(self, world, range)

I could only call the Mover constructor, for example, and simply initialise the Human's _range explicitly, but this jumps out at me as a much worse approach, since it duplicates the initialisation code for an Attacker.

Like I say, I'm aware that setting the _world attribute twice is no big deal, but you can imagine that if something more intensive went on in Actor.__init__, this situation would be a worry. Can anybody suggest a better practice for implementing this structure in Python?

ecatmur
  • 152,476
  • 27
  • 293
  • 366
unwitting
  • 3,346
  • 2
  • 19
  • 20
  • You won't call constructor (better to say 'initializer') twice. That's not how inheritence works. You'd better see MRO topic to clarify Python inheritence for yourself: http://www.python.org/getit/releases/2.3/mro/ – Rostyslav Dzinko Aug 13 '12 at 10:03

1 Answers1

6

What you've got here is called diamond inheritance. The Python object model solves this via the method resolution order algorithm, which uses C3 linearization; in practical terms, all you have to do is use super and pass through **kwargs (and in Python 2, inherit from object):

class Actor(object):    # in Python 3, class Actor:
    _world = None
    def __init__(self, world):
        self._world = world

class Mover(Actor):
    _speed = 0
    def __init__(self, speed, **kwargs):
        super(Mover, self).__init__(**kwargs)    # in Python 3, super().__init__(**kwargs)
        self._speed = speed

class Attacker(Actor):
    _range = 0
    def __init__(self, range, **kwargs):
        super(Attacker, self).__init__(**kwargs) # in Python 3, super().__init__(**kwargs)
        self._range = range

class Human(Mover, Attacker):
    def __init__(self, **kwargs):
        super(Human, self).__init__(**kwargs)    # in Python 3, super().__init__(**kwargs)

Note that you now need to construct Human with kwargs style:

human = Human(world=world, range=range, speed=speed)

What actually happens here? If you instrument the __init__ calls you find that (renaming the classes to A, B, C, D for conciseness):

  • D.__init__ calls B.__init__
    • B.__init__ calls C.__init__
      • C.__init__ calls A.__init__
        • A.__init__ calls object.__init__

What's happening is that super(B, self) called on an instance of D knows that C is next in the method resolution order, so it goes to C instead of directly to A. We can check by looking at the MRO:

>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>)

For a better understanding, read Python’s super() considered super!

Note that super is absolutely not magic; what it does can be approximated in Python itself (here just for the super(cls, obj) functionality, using a closure to bypass __getattribute__ circularity):

def super(cls, obj):
    mro = type(obj).__mro__
    parent = mro[mro.index(cls) + 1]
    class proxy(object):
        def __getattribute__(self, name):
            return getattr(parent, name).__get__(obj)
    return proxy()
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • Thanks a lot for your answer! I've always been a little confused as to what, say, `super(self, Human).__init__` refers to in the `Human` initialiser - is this essentially saying 'I'll give all these named arguments to the initialiser of `Human`, and the MRO needs to look upwards and work out which superclass of `Human` can handle those arguments? – unwitting Aug 13 '12 at 10:25
  • I think if I'd known it was called Diamond Inheritance I might've been able to find some answers on this before posting - I did try, honest! :) – unwitting Aug 13 '12 at 10:26
  • Ah, useful edits to your answer and everything; if I could upvote more I would, sir. That clears things up - I've been reading the article posted above by @RostyslavDzinko and am learning about linearization - this is beginning to make sense :) – unwitting Aug 13 '12 at 10:31
  • 1
    @unwitting it's a lot simpler than that; it means "look at the MRO of `self`, walk along it till I find `Human`, and call the method on the class directly after `Human`". – ecatmur Aug 13 '12 at 10:31