Inheritance

We've met the concept of inheritance a few times earlier in this book. It's time to dive more deeply into inheritance and how it works in Python.

Class Inheritance

Inheritance is a fundamental principle of object-oriented programming that lets classes acquire (inherit or subclass) attributes from another class. When a class named D inherits from a class named B, D is called a subclass or derived class of B. In contrast, class B is called a superclass or base class of D. We will use the subclass and superclass terminology. We sometimes refer to the relationships between subclasses and superclasses as a hierarchy; it describes the "is-a" relationships between classes. (We'll talk about is-a relationships in this chapter.)

Technically, instance variables belong to an object. Thus, they can't be inherited from a class. However, subclass objects acquire the same instance variables as superclass instances. In this chapter, we'll say that they are inherited. However, keep in mind that that isn't really true.

Note that subclasses and superclasses have an ancestor/descendant relationship; it's not strictly one-to-one, as the above description may suggest. Suppose class Y subclasses class X and class Z subclasses class Y. Then Y and Z are both subclasses of X, and X and Y are superclasses of Z. This kind of relationship sometimes leads to terms like child class, parent class, grandparent class, grandchild class, and so on when we need to be more specific about the immediate relationship between two types.

Inheritance lets us extract common behaviors from classes that share those behaviors and move them to a superclass. Thus, we can keep code and logic in one place. Let's look at an example. We'll start with a Car and a Truck class:

class Car:

    def drive(self):
        print('I am driving.')

class Truck:

    def drive(self):
        print('I am driving.')

car = Car()
car.drive()               # I am driving.

truck = Truck()
truck.drive()             # I am driving.

These two classes have identical drive behaviors and may share many other characteristics. We'd like them to share those behaviors as much as possible.

First, we might decide whether cars and trucks belong to a more general class of things. One such common class of things would be vehicles. We can use inheritance to establish that inheritance hierarchy, and our resulting Vehicle class can provide the shared behavior:

#highlight
class Vehicle:

    def drive(self):
        print('I am driving.')

class Car(Vehicle):
    pass

class Truck(Vehicle):
    pass
#endhighlight

car = Car()
car.drive()               # I am driving.

truck = Truck()
truck.drive()             # I am driving.

Here, we've extracted the drive method from the Car and Truck classes to the Vehicle superclass. We've used inheritance to make the drive behavior available to cars and trucks.

We used (Vehicle) on the Car and Truck class statements to tell Python we want to inherit from the Vehicle class. That means that Car and Truck instances can access all of Vehicle's attributes.

When we run this code, we see the correct output. Both classes use the Vehicle.drive method.

Let's add a Motorcycle class to our code:

# Code omitted for brevity

#highlight
class Motorcycle(Vehicle):
    pass

motorcycle = Motorcycle()
motorcycle.drive()  # I am driving.
#endhighlight

Great. That works, too! However, motorcyclists prefer to talk about "riding" instead of driving. Can we fix that and make motorcycles say "I am riding!" instead? We sure can: we can override the inherited drive method in the Motorcycle class:

# Code omitted for brevity

#highlight
class Motorcycle(Vehicle):
    # pass deleted

    def drive(self):
        print('I am riding!')
#endhighlight

motorcycle = Motorcycle()
#highlight
motorcycle.drive()  # I am riding!
#endhighlight

If you rerun the code, you should see:

I am driving.
I am driving.
I am riding!

Here, we're overriding the drive method in the Motorcycle class. Python first checks for a drive method in the Motorcycle class; since it finds one, it invokes that method instead of checking the Vehicle class.

Inheritance can be a great way to remove duplication in your code base. There's an acronym you'll see often in most programming communities: DRY. It stands for "Don't Repeat Yourself", meaning you should extract duplicate code and put it where it can be reused multiple times. Inheritance is one way to DRY your code.

The super Function

Python provides the super function to call methods in the superclass. You don't need to know which class is the superclass, though that's easy to determine. Instead, super returns a placeholder object that acts like an instance of the current object's superclass. We sometimes call this placeholder a proxy object.

The super function is actually a constructor for a class named super.

Once you have the proxy object, you can use it to call a method in the parent class. This technique lets methods that override a superclass method call the overridden method as part of its processing. This sounds more complex than it really is. Let's modify our Motorcycle.drive method to demonstrate how it works:

# Code omitted for brevity

class Motorcycle(Vehicle):
    # pass deleted

    #highlight
    def drive(self):
        super().drive()
        print('  No! I am riding!')
    #endhighlight

motorcycle = Motorcycle()
#highlight
motorcycle.drive()  # I am driving.
                    #   No! I am riding!
#endhighlight

In this example, the Motorcycle.drive method first calls super to retrieve the appropriate proxy object, then uses the proxy to call Vehicle.drive. Once Vehicle.drive does its thing, Motorcycle.drive can do what it must.

The most common way to use super is with the __init__ method. We'll need to expand our classes a bit:

class Vehicle:

    #highlight
    def __init__(self, wheels):
        self._wheels = wheels
        print(f'I have {self._wheels} wheels.')
    #endhighlight

    def drive(self):
        print('I am driving.')

class Car(Vehicle):

    #highlight
    def __init__(self):
        print('Creating a car.')
        super().__init__(4)
    #endhighlight

class Truck(Vehicle):

    #highlight
    def __init__(self):
        print('Creating a truck.')
        super().__init__(18)
    #endhighlight

class Motorcycle(Vehicle):

    #highlight
    def __init__(self):
        print('Creating a motorcycle.')
        super().__init__(2)
    #endhighlight

    def drive(self):
        #highlight
        super().drive()
        #endhighlight
        print('No! I am riding!')

car = Car()         # A car has been created.
                    # I have 4 wheels
car.drive()         # I am driving.
print()

truck = Truck()     # A truck has been created.
                    # I have 18 wheels
truck.drive()       # I am driving.
print()

motorcycle = Motorcycle()
# A motorcycle has been created.
# I have 2 wheels

motorcycle.drive()  # I am driving.
                    # No! I am riding!

Notice that Vehicle.__init__ requires a wheel count argument. The subclass __init__ methods must therefore call super().__init__ to provide that argument.

You can omit a subclass's __init__ method if all it needs to do is call super().__init__ with the same arguments that were provided to the subclass's __init__ method. All other subclasses should have a __init__ method that calls super().__init__, even if the superclass lacks a __init__ method.

You will sometimes see code that includes a __init__ method in a subclass even when it isn't needed. That's fine. Mostly, some developers get in the habit of automatically creating __init__ methods, and do so even when they aren't required. Some teams may also require __init__ methods as part of their style guide.

You also don't need to call super().__init__ when your class inherits from object or doesn't inherit from an explicit class. The object class has a __init__ method that does nothing. Thus, calling it doesn't accomplish anything. It doesn't hurt to call it, though.

A subclass's __init__ method, if it exists, should almost always call super().__init__ before it does anything else. The superclass usually needs to complete initializing the superclass part of the object before the subclass does anything that might rely on it. The above example doesn't stick to that rule, but the print statements are only for demonstration purposes; they don't initialize anything.

Multiple Inheritance

Multiple inheritance (MI) refers to the ability of a class to inherit from multiple superclasses, allowing it to inherit attributes from each of those classes. This feature enables the creation of a new class that combines the attributes of multiple classes.

Suppose we have a program with Pet and Predator classes. We want to create a Cat class with attributes of Pet and Predator. We might end up with code that looks like this:

class Pet:

    def play(self):
        print('I am playing')

class Predator:

    def hunt(self):
        print('I am hunting')

class Cat(Pet, Predator):

    def purr(self):
        print('I am purring')

cat = Cat()
cat.purr()          # I am purring
cat.play()          # I am playing
cat.hunt()          # I am hunting

Here, our Cat class subclasses Pet and Predator. Thus, Cat instances can call methods from any of those classes.

What happens if Pet and Predator also have eat methods? Which method will cat.eat() invoke? Suppose both classes have an instance variable with the same name but a different value? What happens then?

There are rules regarding how Python resolves these problems, but, in the end, multiple inheritance is tricky. As a rule, you should avoid it until you have a lot of experience. Even expert programmers are known to sidestep MI. They know all about the pitfalls of MI and choose to stay away.

Mix-Ins

Mix-ins are another way to achieve polymorphism in Python. They are classes that are never instantiated. Typically, they provide common behaviors to classes that have no apparent hierarchy. For instance, cars, smart lights, and houses all have colors you can change. However, no reasonable inheritance relationship exists that provides the necessary color behaviors: getting and setting the color.

A mix-in is a class that provides behaviors to other classes. They are interface only -- a standard set of methods that can be used wherever needed. Mix-ins are typically small and focused on providing specific functionality.

Technically, mix-ins take advantage of multiple inheritance. While MI has many pitfalls, the mix-in concept avoids most of them. The key is that mix-in classes are behavior-based; they aren't a more general form of the classes that use them.

A mix-in is mixed into a class by adding it to the class statement as though you inherited it. Suppose we have a Car class that has a color that can be changed:

class Car:

    def __init__(self, color):
        self.set_color(color)

    def set_color(self, color):
        self._color = color

    def get_color(self):
        return self._color

car = Car('red')
print(car.get_color())           # red

car.set_color('green')
print(car.get_color())           # green

We also have a SmartLight class which can take on various colors:

class SmartLight:

    def __init__(self, color):
        self.set_color(color)

    def set_color(self, color):
        self._color = color

    def get_color(self):
        return self._color

smart_light = SmartLight('cool white')
print(smart_light.get_color())   # cool white

smart_light.set_color('goldenrod')
print(smart_light.get_color())   # goldenrod

That's a lot of duplicated code! Worse yet, we now want a house class whose exterior can be painted:

class House:

    def __init__(self, color):
        self.set_color(color)

    def set_color(self, color):
        self._color = color

    def get_color(self):
        return self._color

house = House('sky blue')
print(house.get_color())         # sky blue

house.set_color('lavender')
print(house.get_color())         # lavender

Wow. It sure would be nice if we didn't have to reproduce all that code for every class with changeable color choices. We obviously don't want to use inheritance here. There's no common ancestor class that makes sense for all three of these classes. Here's where a mix-in shines:

class ColorMixin:

    def set_color(self, color):
        self._color = color

    def get_color(self):
        return self._color
from color_mixin import ColorMixin

class Car(ColorMixin):

    def __init__(self, color):
        self.set_color(color)

car = Car('red')
print(car.get_color())           # red

car.set_color('green')
print(car.get_color())           # green
from color_mixin import ColorMixin

class SmartLight(ColorMixin):

    def __init__(self, color):
        self.set_color(color)

smart_light = SmartLight('cool white')
print(smart_light.get_color())   # cool white

smart_light.set_color('goldenrod')
print(smart_light.get_color())   # goldenrod
from color_mixin import ColorMixin

class House(ColorMixin):

    def __init__(self, color):
        self.set_color(color)

house = House('sky blue')
print(house.get_color())         # sky blue

house.set_color('lavender')
print(house.get_color())         # lavender

Perfect. We eliminated six method definitions from the original three classes and added two in the new mix-in class. More importantly, we can add our ColorMixin mix-in to any class that requires the ability to set and retrieve color.

It's worth noting that we've named our mix-in class with a Mixin suffix. This is a common Python convention. It ensures that users of the mix-in know that it's a mix-in, not an ordinary class that can be instantiated.

"Is-a" vs "Has-a"

We typically use nouns when discussing classes; they represent concrete (non-abstract) things. The objects we create from classes are specific instances of those things. If we create a my_car object from the Car class, we say that my_car is a Car. This is shorthand for saying that the object referenced by my_car is an instance of the Car class.

We also use the is-a terminology to describe inheritance relationships. For instance, if our Car class inherits from a Vehicle class, then a Car object is a Vehicle object, or, more succinctly, a Car is a Vehicle.

Classes and mix-ins are often said to have a has-a or have-a relationship. In our ColorMixin example earlier, we can say that cars, smart lights, and houses have a color property that we can get and set.

The has-a relationship is also used with instance variables. A Person class might have a name and an age. That is, a person has a name and age.

The Is-A Relationship

Let's look a little closer at the is-a relationship with a class hierarchy. Cars are a particular kind of vehicle; a car is a vehicle. That means a Car class can subclass a Vehicle class. Likewise, trucks are another kind of vehicle, so we can add a Truck class that also inherits from Vehicle:

class Vehicle:
    pass

class Car(Vehicle):
    pass

class Truck(Vehicle):
    pass

car = Car()
print(isinstance(car, Car))       # True
print(isinstance(car, Vehicle))   # True
print(isinstance(car, Truck))     # False

truck = Truck()
print(isinstance(truck, Vehicle)) # True
print(isinstance(truck, Car))     # False

The isinstance calls show that a Car object is a Car and it is also a Vehicle. However, a Car object is not a Truck even though a Truck is a Vehicle.

The key here is that a Car is a Vehicle. This lets us justify the inheritance relationships. However, not all is a relationships should involve inheritance. For example, a well-known counterexample is squares and rectangles. Since a square is a rectangle, you might think a Square class should inherit from a Rectangle class. However, that's not the case.

The reason for this is somewhat subtle, but let's see why it's a problem:

class Rectangle:

    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        my_area = self.width * self.height
        print(f'area is {my_area}')

class Square(Rectangle):

    def __init__(self, size):
        super().__init__(size, size)

    def set_size(self, size):
        self.width = size
        self.height = size

square = Square(7)
square.area()             # area is 49

square.set_size(12)
square.area()             # area is 144

square.width = 5
square.height = 9
square.area()             # area is 45

Oops! Our square is no longer a square! It has the behaviors of a square, but it has the characteristics of a rectangle. This makes for poor design.

There are several ways to avoid rectangular squares while retaining the "squares are rectangles" idea, but they all lead to complex code. The main problem here is that squares and rectangles have different interfaces.

The primary way to fix this code is to use a more generic Shape class as the superclass and then customize the subclasses.

class Shape:

    def area(self):
        pass

class Rectangle(Shape):

    def __init__(self, width, height):
        self._width = width
        self._height = height

    def set_width(self, width):
        self._width = width

    def set_height(self, height):
        self._height = height

    def area(self):
        my_area = self._width * self._height
        print(f'area is {my_area}')

class Square(Shape):

    def __init__(self, size):
        self._size = size

    def set_size(self, size):
        self._size = size

    def area(self):
        my_area = self._size * self._size
        print(f'area is {my_area}')

square = Square(7)
square.area()             # area is 49

square.set_size(12)
square.area()             # area is 144

square.width = 5
square.height = 9
square.area()             # area is 144

Note that we gave the Shape class a do-nothing area method. The sole purpose of this method is to provide the API for the Shape class and all its subclasses. Subclass designers are obligated to provide the implementation of that method.

Many OOP coding examples -- including some in this book -- use inappropriate class hierarchies. That's done primarily to simplify the concepts involved; it isn't meant to demonstrate good OOP practices.

Inheritance relationships should have an "is-a" relationship. If class A subclasses class B, then objects of type A must also be usable as objects of type B. Before using inheritance, you should always ask yourself, "Does my class have an is-a relationship to the class I'm subclassing?" If it doesn't, then inheritance may be inappropriate. Even if it is, be careful about inheriting from classes with incompatible APIs.

The Has-A Relationship

Suppose you're writing a class that doesn't have an is-a relationship with a class you wish to inherit from. In that case, you should consider whether it has a has-a relationship instead. If it does, you may want to explore the possibilities of using a mix-in or composition.

Composition is a design principle where a class uses one or more objects of other classes to provide some of the composing class's functionality. This lets the programmer create more modular, manageable, and adaptable code by favoring a "has-a" relationship (composition) over an "is-a" relationship (inheritance). In composition, the composing class can access and use the functionalities of composited objects. Thus, the composing class can delegate specific responsibilities to the composited objects.

We often speak of composition as a form of collaboration. Collaborators are objects a class interacts with to perform its responsibilities and functionality.

Merely having an object inside your class isn't collaboration. At least one of the class's instance methods must use that object to aid the containing class's behavior.

A container class such as a list can be a collaborator in one of your classes if your class uses the list in some way in service of its functions. However, the members of that list may or may not be collaborators themselves; that depends on whether your class uses them to perform additional actions.

We can demonstrate composition by rewriting our shapes.py program to compose a Shape object into the Rectangle and Square classes. In this example, both the Rectangle and Square classes use a Shape object as a collaborator.

class Shape:

    def __init__(self, width, height):
        self.set_size(width, height)

    def set_size(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        my_area = self.width * self.height
        print(f'area is {my_area}')

class Rectangle:

    def __init__(self, width, height):
        self._shape = Shape(width, height)

    def set_width(self, width):
        self._shape.set_size(width, self._shape.height)

    def set_height(self, height):
        self._shape.set_size(self._shape.width, height)

    def area(self):
        self._shape.area()

class Square:

    def __init__(self, size):
        self._shape = Shape(size, size)

    def set_size(self, size):
        self._shape.set_size(size, size)

    def area(self):
        self._shape.area()

# Test code omitted

In this example, we no longer inherit from Shape. Instead, both original subclasses instantiate a Shape object with which they collaborate. Class users can still cheat and create non-square Square objects. However, the leading underscore naming convention designates self._shape for internal use. If users violate that convention, they do so at their own risk.

Object relationships that use collaborators or mix-ins should have a "has-a" relationship. Suppose class A uses a mix-in called B and a collaborator object of class C. We can say that an object of class A has the behaviors provided by the B mix-in. We can also say that an object of type A has a collaborator object of class C.

Many developers prefer has-a relationships to is-a relationships. This principle is called Composition Over Inheritance (COI). It suggests that using mix-ins and composition in preference to inheritance is more flexible and safer.

As with questionable inheritance hierarchies, many OOP coding examples ignore the COI principle. The mechanics of inheritance are fundamental, though that importance in everyday programming is lessening over time. However, you can't appreciate the COI until you've seen and wrestled with inheritance.

Method Resolution Order (MRO)

When you call a method, how does Python know where to look for that method? Python has a distinct lookup path that it follows each time a method is called. It's called the Method Resolution Order, or MRO. Let's define a set of classes, subclasses, and mix-ins that we can use as an example.

class LandDwellingMixin:
    pass

class LanguageMixin:
    pass

class BipedalismMixin:
    pass

class Creature:
    pass

class Mammal(Creature):
    pass

class Primate(LandDwellingMixin, Mammal):
    pass

class Human(BipedalismMixin,
            LanguageMixin,
            Primate):
    pass

print(Human.mro())
# Pretty printed for clarity
[
    <class '__main__.Human'>,
    <class '__main__.BipedalismMixin'>,
    <class '__main__.LanguageMixin'>,
    <class '__main__.Primate'>,
    <class '__main__.LandDwellingMixin'>,
    <class '__main__.Mammal'>,
    <class '__main__.Creature'>,
    <class 'object'>
]

Note that we've used the class.mro method on the Human class to determine the MRO for that class; you can apply it to any class.

All Python classes are instances of the type metaclass. A metaclass is a class that creates other classes. As a result, classes can call methods defined on type. mro is a class method defined by the type metaclass.

This output shows us how Python searches for a method when we use the Human class to invoke a method. Note that the search ends only when the method is found or Python can't find it.

The search is a modified "depth first" search that considers all items listed in the inheritance list, even those that are being used as mix-ins. The resulting search is recursive and difficult to describe clearly.

If we assume that mix-ins are listed first in the inheritance list, and no more than one superclass is listed, you can describe the process with pseudocode:

  • set the current class to the class of the calling object
  • while the current class is not None:
    • if the current class has the method, stop searching
    • for each mix-in in the current class's inheritance list:
      • if the mix-in has the method, stop searching
    • set the current class to the superclass of the current class
  • raise an AttributeError

This pseudocode falls apart if any mix-ins are listed after a superclass or if the inheritance list has multiple superclasses. However, the MRO is determined by going through the items in the inheritance list from left-to-right, and for each item in the list, exploring all of that items superclasses and mix-ins.

Recall that every Python class has object as its ultimate superclass, but the object class itself has no superclass. That means the while loop in the above pseudocode keeps running until the method is found or the object class has been searched without finding the method. Note that the current class will be None after the object class is searched.

Let's use the pseudocode and the mro.py classes and mix-ins to see how the MRO works. Let's assume we want to invoke a method named foo using a Human instance, and that the Mammal class is where the foo method exists:

# code omitted for brevity

class Mammal(Creature):
    def foo(self):
        pass

# code omitted for brevity

paul = Human()
paul.foo()

Python begins the search by setting the current class to the class of the calling object (paul). That class is Human.

  • Is the current class set to None?
  • No; It is Human.
    • Does Human define the foo method?
    • No, it does not.
      • For each mix-in in Human's inheritance list:
        • The first mix-in is BipedalismMixin:
          • Does BipedalismMixin have a foo method?
          • No. Continue searching.
        • The second mix-in is LanguageMixin:
          • Does LangageMixin have a foo method?
          • No. Continue searching.
        • There are no more mix-ins for Human.
    • Set the current class to Human's superclass (Primate)

We now return to the top of the while loop in the pseudocode. The current class is now Primate:

  • Is the current class set to None?
  • No; It is Primate.
    • Does Primate define the foo method?
    • No, it does not.
      • For each mix-in in Primate's inheritance list:
        • The only mix-in is LandDwellingMixin:
          • Does LandDwellingMixin have a foo method?
          • No. Continue searching.
        • There are no more mix-ins for Primate.
    • Set the current class to Primate's superclass (Mammal)

Once again, we're the top of the while loop in the pseudocode. The current class is now Mammal:

  • Is the current class set to None?
  • No; It is Mammal.
    • Does Mammal define the foo method?
    • Yes, it does!
      • Invoke Mammal's foo method using original Human object (paul).
      • Terminate the search.

Had the foo method been defined in LanguageMixin, then Python would have found that version of foo instead. Had foo not been defined in Mammal or any other class or mix-in, the search would have continued first to the Creature class, and then the object class. Once object was searched without finding the method, the current class would be set to None, which would have led to an AttributeError.

Note that the MRO is not used to look up instance variables in an inheritance chain. Instance variables are tied to instances, not to the classes or mix-ins used when defining the original class.

Summary

We've covered a lot of ground in this chapter and book. You should feel pretty comfortable with the general syntax and structure of the OO parts of Python. You've got one more set of exercises to help put this information to good use. Then, you'll be ready to take the next step on your way to becoming a Python developer.

All this complex knowledge about OOP is meant to help us design and build better applications. While there are definitely wrong ways to design an application, there is often no right choice when it comes to object-oriented design, only different tradeoffs. As you gain more experience in object-oriented design, you'll develop an intuition for organizing and shaping your classes. All this may feel a little daunting for now, but once you learn how to think in an OO way, it's hard to not think in that manner.

Finally, make sure to take time to go through the exercises. OOP is a tricky concept if this is your first time encountering it. Even if you've programmed in another OO language before, Python's implementation may be a little different. It's not enough to read and understand; you must learn by doing. Let's get on to the exercises!

Exercises

  1. For each of the following pairs of classes, try to determine whether they have an "is-a" or "has-a" relationship or neither.

    First Class Second Class
    Car Engine
    Teacher Student
    Flag Color
    Apple Orange
    Ship Vessel
    Structure Home
    Shape Circle

    Solution

    A car can have an engine, so this is a has-a relationship.

    A teacher is not generally a student, and a student is not generally a teacher. However, a teacher can have a student, or a student can have a teacher. No matter how you look at it, this is a has-a relationship.

    A flag is not a color, nor is a color a flag. However, a flag can have several colors, so this is a has-a relationship.

    An apple is not an orange, nor is an orange an apple. Likewise, apples don't have oranges, and oranges don't have apples. This is neither an is-a nor a has-a relationship.

    A ship is a vessel, so this is an is-a relationship.

    A house is a structure, so this is an is-a relationship. A house can also have multiple structures associated with it: a garage, a tool shed, an outhouse, etc. That means this relation can also be seen as a has-a relationship from the house's perspective.

    A circle is a shape, so this is an is-a relationship.

    Some of the class names in this exercise may have multiple meanings. For instance, we use the shipping-based definition for vessels. However, vessels are also defined as containers like bottles and kettles. If we use the container definition, there is no relationship between a ship and a vessel. The correct answers may vary based on how you interpreted the class names.

  2. Write the code needed to make the following code work as shown:

    print(Car.vehicles())     # 0
    car1 = Car()
    print(Car.vehicles())     # 1
    car2 = Car()
    car3 = Car()
    car4 = Car()
    print(Car.vehicles())     # 4
    truck1 = Truck()
    truck2 = Truck()
    print(Truck.vehicles())   # 6
    boat1 = Boat()
    boat2 = Boat()
    print(Boat.vehicles())    # 8
    

    Solution

    class Vehicle:
        number_of_vehicles = 0
    
        def __init__(self):
            Vehicle.number_of_vehicles += 1
    
        @classmethod
        def vehicles(cls):
            return Vehicle.number_of_vehicles
    
    class Car(Vehicle):
    
        def __init__(self):
            super().__init__()
    
    class Truck(Vehicle):
    
        def __init__(self):
            super().__init__()
    
    class Boat(Vehicle):
    
        def __init__(self):
            super().__init__()
    
    # Test code omitted
    

    For this problem, the easiest solution is to use a Vehicle superclass that keeps track of how many objects it creates. We need a class variable and a __init__ method that increments the class variable each time a new vehicle is constructed.

    We also need a __init__ method in the Car, Truck, and Boat classes, all of which call the Vehicle.__init__ method using super().__init__. These subclass __init__ methods aren't strictly required in this rudimentary code, but it's good practice.

    Finally, notice that we can call Vehicle.vehicles using any of the four classes used in this problem.

  3. Create a mix-in for the Car and Truck classes from the previous exercise that lets you operate the turn signals: signal left, signal right, and signal off. Use the following code to test your code.

    car1.signal_left()       # Signalling left
    truck1.signal_right()    # Signalling right
    car1.signal_off()        # Signal is now off
    truck1.signal_off()      # Signal is now off
    boat1.signal_left()
    # AttributeError: 'Boat' object has no attribute
    # 'signal_left'
    

    Solution

    #highlight
    class SignalMixin:
    
        def signal_left(self):
            print('Signalling left')
    
        def signal_right(self):
            print('Signalling right')
    
        def signal_off(self):
            print('Signal is now off')
    #endhighlight
    
    # Vehicle class omitted for brevity
    
    #highlight
    class Car(SignalMixin, Vehicle):
    #endhighlight
    
        def __init__(self):
            super().__init__()
    
    #highlight
    class Truck(SignalMixin, Vehicle):
    #endhighlight
    
        def __init__(self):
            super().__init__()
    
    # Remaining code omitted for brevity
    
  4. Print the method resolution order for cars, trucks, boats, and vehicles as defined in the previous exercise.

    Solution

    # Code omitted for brevity
    
    # Comment out this line
    #boat1.signal_left()
    
    print(Car.mro())
    print(Truck.mro())
    print(Boat.mro())
    print(Vehicle.mro())
    
    # Pretty printed for clarity
    [
        <class '__main__.Car'>,
        <class '__main__.SignalMixin'>,
        <class '__main__.Vehicle'>,
        <class 'object'>
    ]
    
    [
        <class '__main__.Truck'>,
        <class '__main__.SignalMixin'>,
        <class '__main__.Vehicle'>,
        <class 'object'>
    ]
    
    [
        <class '__main__.Boat'>,
        <class '__main__.Vehicle'>,
        <class 'object'>
    ]
    
    [
        <class '__main__.Vehicle'>,
        <class 'object'>
    ]
    
  5. We've provided new Car and Truck classes and some tests below. Refactor them to use inheritance for as much behavior as possible. The tests shown in the code should still work as shown:

    class Car:
    
        def __init__(self, fuel_capacity, mpg):
            self.capacity = fuel_capacity
            self.mpg = mpg
    
        def max_range_in_miles(self):
            return self.capacity * self.mpg
    
        def family_drive(self):
            print('Taking the family for a drive')
    
    class Truck:
    
        def __init__(self, fuel_capacity, mpg):
            self.capacity = fuel_capacity
            self.mpg = mpg
    
        def max_range_in_miles(self):
            return self.capacity * self.mpg
    
        def hookup_trailer(self):
            print('Hooking up trailer')
    
    car = Car(12.5, 25.4)
    truck = Truck(150.0, 6.25)
    
    print(car.max_range_in_miles())         # 317.5
    print(truck.max_range_in_miles())       # 937.5
    
    car.family_drive()     # Taking the family for a drive
    truck.hookup_trailer() # Hooking up trailer
    
    try:
        truck.family_drive()
    except AttributeError:
        print('No family_drive method for Truck')
    # No family_drive method for Truck
    
    try:
        car.hookup_trailer()
    except AttributeError:
        print('No hookup_trailer method for Car')
    # No hookup_trailer method for Car
    

    Solution

    class Vehicle:
    
        def __init__(self, fuel_capacity, mpg):
            self.capacity = fuel_capacity
            self.mpg = mpg
    
        def max_range_in_miles(self):
            return self.capacity * self.mpg
    
    class Car(Vehicle):
    
        def __init__(self, fuel_capacity, mpg):
            super().__init__(fuel_capacity, mpg)
    
        def family_drive(self):
            print('Taking the family for a drive')
    
    class Truck(Vehicle):
    
        def __init__(self, fuel_capacity, mpg):
            super().__init__(fuel_capacity, mpg)
    
        def hookup_trailer(self):
            print('Hooking up trailer')
    
    car = Car(12.5, 25.4)
    truck = Truck(150.0, 6.25)
    
    print(car.max_range_in_miles())         # 317.5
    print(truck.max_range_in_miles())       # 937.5
    
    car.family_drive()     # Taking the family for a drive
    truck.hookup_trailer() # Hooking up trailer
    
    try:
        truck.family_drive()
    except AttributeError:
        print('No family_drive method for Truck')
    # No family_drive method for Truck
    
    try:
        car.hookup_trailer()
    except AttributeError:
        print('No hookup_trailer method for Car')
    # No hookup_trailer method for Car