Search code examples
pythoninheritancemultiple-inheritancediamond-problem

One-Many-One Inheritance in Python


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?


Solution

  • 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()