Execute Program

Python in Detail: Multiple Inheritance

Welcome to the Multiple Inheritance lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • An earlier lesson showed that classes can inherit methods and attributes from their base classes. A class can also inherit from multiple base classes, which gives it access to all parent classes' methods and attributes.

  • In the next example, Cat inherits from Vertebrate and Pet.

  • >
    class Vertebrate:
    def has_skeleton(self):
    return True

    class Pet:
    def domesticated(self):
    return True

    class Cat(Vertebrate, Pet):
    pass

    ms_fluff = Cat()
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    ms_fluff.has_skeleton()
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    ms_fluff.domesticated()
    Result:
    TruePass Icon
  • When we call ms_fluff.domesticated(), Python has to check multiple classes to find that method. We can think of Python doing the following checks, in the following order, until it finds the method.

    1. Is .domesticated defined on the instance itself?
    2. Is .domesticated defined on Cat (this object's class)?
    3. Is .domesticated defined on Vertebrate (the first parent class)?
    4. Is .domesticated defined on Pet (the second parent class)?
    5. If we got this far, give up and raise an AttributeError.
  • In the fourth step, Python finds Pet.domesticated, so that's what it calls.

  • We call the sequence of checking Cat, then Vertebrate, then Pet the "method resolution order", often abbreviated as MRO. The MRO depends on the parent class order. For example, if we define our class as class Cat(Pet, Vertebrate) instead, then Python checks Pet before Vertebrate.

  • When multiple parent classes define methods with the same name, the first matching class in the MRO wins. For example, both TVs and computers display things.

  • >
    class TV:
    def display(self):
    return "movie"

    class Computer:
    def display(self):
    return "blue screen"

    class SmartTV(TV, Computer):
    pass

    smart_tv = SmartTV()
    smart_tv.display()
    Result:
    'movie'Pass Icon
  • In class SmartTV(TV, Computer), TV comes first, so it wins.

  • >
    class Phone:
    def display(self):
    return "call"

    class Computer:
    def display(self):
    return "blue screen"

    class SmartPhone(Computer, Phone):
    pass

    phone = SmartPhone()
    phone.display()
    Result:
    'blue screen'Pass Icon
  • In class SmartPhone(Computer, Phone), Computer comes first, so it wins.

  • Let's summarize that. Inheritance order determines method resolution order. Method resolution order determines which methods are actually called at runtime. Calling different methods changes runtime behavior. So changing inheritance order also changes runtime behavior.

  • We've seen that Python is a dynamic language: we can inspect and change our code's behavior at runtime. The MRO is another example of that: we can directly inspect the method resolution order via SomeClass.__mro__. This gives us a tuple of classes showing the actual method resolution order that Python uses.

  • >
    class Phone:
    def display(self):
    return "call"

    class Computer:
    def display(self):
    return "program"

    class SmartPhone(Phone, Computer):
    pass

    [cls.__name__ for cls in SmartPhone.__mro__]
    Result:
  • The last class in the MRO is 'object', but we never inherited from that explicitly. It's there because object is the parent class of all other classes in Python.

  • We can explicitly inherit from object to see that it doesn't affect the MRO. (But note that we never need to do this in real-world code!)

  • >
    class Phone(object):
    pass

    class Computer(object):
    pass

    class SmartPhone(Phone, Computer):
    pass

    [cls.__name__ for cls in SmartPhone.__mro__]
    Result:
    ['SmartPhone', 'Phone', 'Computer', 'object']Pass Icon
  • The code above highlights a subtle point: a class shows up only once in the MRO. Phone and Computer each have object as a base class, and SmartPhone inherits from both. However, the MRO only includes the object class once. That's because there's no reason to check for an attribute multiple times.

  • Multiple inheritance enables some design patterns that would be awkward otherwise, like mixins. Mixins are classes that provide methods to other classes via inheritance, but aren't designed to be instantiated directly.

  • Here are two classes: a regular Cow class, and a Circle mixin class, which calculates overlap with a coordinate.

  • >
    class Cow:
    def __init__(self, name):
    self.name = name

    class Circle:
    def overlaps(self, other):
    distance_squared = (self.x - other.x)**2 + (self.y - other.y)**2
    return distance_squared < (self.radius + other.radius)**2
  • We can tell that Cow is meant to be instantiated. Why else would it have an .__init__ method?

  • The Circle class is a mixin. Its .overlaps method accesses self.x, self.y, and self.radius, but Circle doesn't define any of those attributes. It expects some other class in the MRO to define them.

  • Now imagine that we're working on a 2D game with cow characters. Collision detection in the game requires us to know whether two cows overlap. We define a class that's both a Cow and a Circle.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    class CircularCow(Cow, Circle):
    def __init__(self, name, x, y, radius):
    super().__init__(name)
    self.x = x
    self.y = y
    self.radius = radius
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    # Bessie is located at coordinates (0, 0), with a radius of 2.
    bessie = CircularCow("Bessie", 0, 0, 2)

    # Daisy is located at coordinates (1, 1), also with a radius of 2.
    daisy = CircularCow("Daisy", 1, 1, 2)

    # We've modeled the cows as circles. Each circles's radius is larger than
    # the distance between the circles, so the circles overlap, occupying the
    # same space.
    bessie.overlaps(daisy)
    Result:
    TruePass Icon
  • We "mixed in" the .overlaps method into our Cow class, giving us overlap functionality. This might seem a bit strange: Circle is accessing attributes that are only defined in its child class. But Python doesn't care which of the classes defined an attribute. It only cares that the attribute exists on the object.

  • One final note on this example. CircularCow's .__init__ method calls super().__init__. Only one of our superclasses, Cow, has a .__init__ method, so super().__init__(...) calls that constructor.

  • What if both superclasses, Cow and Circle, had .__init__ methods? How can one super().__init__(...) call simultaneously call multiple parent classes' .__init__ methods? We'll see the answer a future lesson.

  • Here's a code problem:

    The Admin class should be a User with the IsAdmin mixin. Finish the Admin class by subclassing from both parent classes. Note that the inheritance order matters, since both classes define the same .is_admin method!

    class User:
    def __init__(self, name):
    self.name = name

    def is_admin(self):
    return False

    class IsAdmin:
    def is_admin(self):
    return True
    class Admin(IsAdmin, User):
    pass
    amir = Admin('Amir')
    assert amir.name == 'Amir'
    assert amir.is_admin() == True
    Goal:
    None
    
    Yours:
    None
    Pass Icon