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.
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.
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 (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 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.
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.
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.
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.
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:
None
:
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
.
None
?
Human
.
Human
define the foo
method?
Human
's inheritance list:
BipedalismMixin
:
BipedalismMixin
have a foo
method?
LanguageMixin
:
LangageMixin
have a foo
method?
Human
.
Human
's superclass (Primate
)
We now return to the top of the while loop in the pseudocode. The current class is now Primate
:
None
?
Primate
.
Primate
define the foo
method?
Primate
's inheritance list:
LandDwellingMixin
:
LandDwellingMixin
have a foo
method?
Primate
.
Primate
's superclass (Mammal
)
Once again, we're the top of the while loop in the pseudocode. The current class is now Mammal
:
None
?
Mammal
.
Mammal
define the foo
method?
Mammal
's foo
method using original Human
object (paul
).
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.
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!
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 |
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.
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
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.
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'
#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
Print the method resolution order for cars, trucks, boats, and vehicles as defined in the previous exercise.
# 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'>
]
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
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