Inheritance

Inheritance is a way to reuse code and data from a parent class. This allow us to avoid repeating ourselves and to build upon existing functionality. This module defines a basic vehicle class, creates a car class that inherits from vehicle, then creates a truck class that inherits from car and use it for demonstration purposes.

from inspect import isfunction, ismethod


class Vehicle:
    """Basic definition of a vehicle.

    We begin with a simple mental model of what a vehicle is. It has
    a make, model, year, and miles. That way, we can start exploring
    the core concepts that are associated with a class definition.
    """

    def __init__(self, make, model, year, miles):
        """Construct a vehicle with make, model, year, and miles."""
        self.make = make
        self.model = model
        self.year = year
        self.miles = miles

    def __repr__(self):
        """Return the formal representation of a vehicle."""
        return f"<Vehicle make={self.make} model={self.model} year={self.year}>"

    def __str__(self):
        """Return the informal representation of a vehicle."""
        return f"{self.make} {self.model} ({self.year})"

    def drive(self, rate_in_mph):
        """Drive a vehicle at a certain rate in MPH."""
        return f"{self} is driving at {rate_in_mph} MPH"


class Car(Vehicle):
    """Definition of a car.

    We inherit from the vehicle class to reuse the code and data that
    we have already defined. In addition, we add a new attribute called
    `wheels` to the car class. This is an example of extending the
    functionality of a parent class. In __init__, we call the parent
    constructor with the `super` function. This is a way to invoke the
    parent constructor without having to explicitly name the parent
    class. We also override the method `__repr__` to have a different
    output than the vehicle.
    """

    def __init__(self, make, model, year, miles):
        """Construct a car with make, model, year, miles, and wheels."""
        super().__init__(make, model, year, miles)
        self.wheels = 4

    def __repr__(self):
        """Return the formal representation of a car."""
        return f"<Car make={self.make} model={self.model} year={self.year} wheels={self.wheels}>"


class Truck(Vehicle):
    """Definition of a truck.

    We inherit from vehicle just like we did with the car class. In this case we
    will also override the method `drive` to have a different output than the
    car and the vehicle.
    """

    def __init__(self, make, model, year, miles):
        """Construct a truck with make, model, year, miles, and wheels."""
        super().__init__(make, model, year, miles)
        self.wheels = 6

    def __repr__(self):
        """Return the formal representation of a truck."""
        return f"<Truck make={self.make} model={self.model} year={self.year} wheels={self.wheels}>"

    def drive(self, rate_in_mph):
        """Drive a truck at a certain rate in MPH."""
        return f"{self} is driving a truck at {rate_in_mph} MPH"


def main():
    # Create a vehicle with the provided class constructor
    vehicle = Vehicle("Mistery Machine", "Van", 1969, 100000.0)

    # Formal representation
    assert repr(vehicle) == "<Vehicle make=Mistery Machine model=Van year=1969>"

    # Informal representation
    assert str(vehicle) == "Mistery Machine Van (1969)"

    # Call a method on the class constructor
    assert vehicle.drive(50) == "Mistery Machine Van (1969) is driving at 50 MPH"

    # Check the type of the method drive
    assert ismethod(vehicle.drive) and not isfunction(vehicle.drive)

    # Now we create a car with the provided class constructor
    car = Car("DeLorean", "DMC-12", 1982, 220000.0)

    # The informal representation is similar to the vehicle
    assert str(car) == "DeLorean DMC-12 (1982)"

    # But the formal representation is different because we included
    # the wheels attribute
    assert repr(car) == "<Car make=DeLorean model=DMC-12 year=1982 wheels=4>"

    # And we can check the type of the method drive like we did with
    # the vehicle
    assert ismethod(car.drive) and not isfunction(car.drive)

    # If we call the method drive, we can see that we did not
    # write any code for the car class, but we can still use it
    # because it is inherited from the vehicle class and the
    # behavior is the same as the vehicle
    assert car.drive(50) == "DeLorean DMC-12 (1982) is driving at 50 MPH"

    # Now we create a truck with the provided class constructor
    truck = Truck("Optimus Prime", "Truck", 1984, 1000000.0)

    # Like car and vehicle, the informal representation is similar
    assert str(truck) == "Optimus Prime Truck (1984)"

    # And the formal representation is different from the vehicle
    # because we included the wheels attribute
    assert repr(truck) == "<Truck make=Optimus Prime model=Truck year=1984 wheels=6>"

    # And we can check the type of the method drive like we did with
    # the vehicle
    assert ismethod(truck.drive) and not isfunction(truck.drive)

    # For the last part, we can see that the method drive is different
    # for the truck
    assert truck.drive(50) == "Optimus Prime Truck (1984) is driving a truck at 50 MPH"


if __name__ == "__main__":
    main()