Python Inheritance
Inheritance in Python
Inheritance in Python
Now that we’ve learned a bit about the theory behind inheritance in object-oriented programming, let’s dive into the code and see how we can accomplish this in Python.
For these examples, we’ll be implementing the following UML diagram:
Above is an example of a UML that is not Python specific. For example in the class Truck, has a method Truck(name:string, horsepower: float)
, in Python this would be a method __init__(string ,float)
.
The UML diagram above includes some items that we haven’t discussed yet. Don’t panic! We’ll cover them as they become relevant in this module.
For now, feel free to ignore the fact that methods are italicized. Similarly, you can treat a hash symbol #
in front of an attribute or method the same as a plus +
that denotes they should be public.
We’ll learn what each of these indicate later in this module.
The first step is to build the inheritance relationships. Let’s start by just declaring the Vehicle
class for now. Remember that the Vehicle
class should be defined in Vehicle.py
. We won’t add any methods or attributes at this point:
class Vehicle:
pass
As you’ll recall, a class declaration is pretty simple. We’ll include a pass
keyword just to let Python know that we are intending to leave the class blank for now.
Next, let’s declare the MotorVehicle
class in MotorVehicle.py
. This class inherits from the Vehicle
class, so we’ll need to use a new syntax to make that work:
from Vehicle import *
class MotorVehicle(Vehicle):
pass
In the example above, we’ve included the parent class Vehicle
inside of parentheses after the name of the child class MotorVehicle
. We also must remember to import the Vehicle
class at the top of the file. That’s all we need to do to show that a class inherits from another class.
Let’s see if we can do the same process for the Car
, Truck
, and Airplane
classes that are shown in the UML diagram above. We must make sure we place the code for each class in the correct file. Use the tests below to make sure that you have your classes structured correctly.
This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.
Now that we’ve created some classes that follow different inheritance relationships, let’s explore how we can use those relationships to change some methods in each class through the use of method overriding.
Vehicle
ClassFirst, let’s look at a couple of methods in the Vehicle
class, the move()
and describe()
methods. As we might recall, the describe()
method is an abstract method since it is in italicized text in the UML diagram. We’ll discuss how to build an abstract method later in this chapter, but for now we’ll just implement it as a normal method that does nothing. Likewise, we haven’t discussed what a hash symbol #
means in front of the speed
attribute or the constructor, so we’ll just make them public for now.
class Vehicle:
def __init__(self, name):
self.__name = name
self.speed = 1.0
@property
def name(self):
return self.__name
def move(self, distance):
print("Moving")
return distance / self.speed
def describe(self):
return None
That’s a very simple implementation of the Vehicle class. The move()
method simply accepts a distance to move as a floating point number, and then divides that by the speed of the vehicle to get the time it takes to go that distance. For the default vehicle, we’ll assume that it moves at a speed of 1.0.
Now that we’ve created some methods in the Vehicle class, let’s go back to the Airplane
class and see how we can build it. First, we’ll need to add the attributes and the constructor for this class:
from Vehicle import *
class Airplane(Vehicle):
def __init__(self, name, wingspan, capacity):
self.__name = name
self.__wingspan = wingspan
self.__capacity = capacity
Before we go any further, let’s stop there and run this code to make sure that it works. For starters, let’s add some quick code to the Main
class in Main.py
that instantiates an Airplane object and tries to print the name
attribute, using the property defined in the Vehicle
class:
from Airplane import *
class Main:
@staticmethod
def main():
a = Airplane("Test", 123, 45)
print(a.name)
# main guard
if __name__ == "__main__":
Main.main()
Once we have that code, we can use the Python interpreter to run it. So, to do that, we can use the Python - Run File option in the run menu above with the Main.py
file selected, or we can open the terminal in Codio and use the following two commands to open the directory containing these files and then execute Main:
cd ~/workspace/12p-inherit/vehicle
python3 Main.py
When we do that, we’ll get some errors as shown in this screenshot:
In this error, it says that the name
attribute in the Vehicle
class doesn’t exist. This is because we haven’t actually called the constructor for the Vehicle
class, so that attribute has not been defined yet. So, when we try to access the name
property defined in the Vehicle
class, it can’t give us the value we want. This is because the name
attribute in the Vehicle
class is actually defined in the constructor for that class.
So, to set the value of the name
attribute, we’ll need to somehow provide that value to the constructor of the Vehicle
class. But wait, why do we need to call the constructor and create a Vehicle
object? Aren’t we just trying to create an Airplane
object?
One of the major things to recall when inheriting from a class is that each object instantiated from the child class is also an instance of the parent class. We can think of it as though the child object contains an instance of the parent object. Because of that, when we try to create an instance of the child class—Airplane
in this example—that class’s constructor must also be able to call the constructor for the parent class, or Vehicle
.
We run into a snag, however, because we’ve provided a constructor in Vehicle
that requires an argument. In that case, we must provide an argument to the constructor for Vehicle
in order to instantiate that object, since the default constructor is no longer available. How can we do that?
Thankfully, there is a quick and easy way to handle this in Python. Inside of our constructor, we can use the special method super()
to reference the parent class, allowing us to call that class’s constructor—its __init__()
method—directly. We can provide any needed arguments to that method call, which are then provided to the constructor in the parent class. So, let’s update our constructor to use a call to super().__init__()
:
from Vehicle import *
class Airplane(Vehicle):
def __init__(self, name, wingspan, capacity):
super().__init__(name)
self.__wingspan = wingspan
self.__capacity = capacity
Now, inside of the constructor for Airplane
, we have added the line super.__init__(name)
, which calls the constructor of the parent class Vehicle
, providing name
as the argument for the parameter. This will resolve the error. We can try it for ourselves by running the code in the Main
class again and seeing that it produces the correct output.
Finally, we can explore how to override a method in our child class. In fact, it is as simple as providing a method declaration that uses the same method signature as the original method. A method signature in programming describes the method name and the order of parameters defined in the function. So, we can override the move()
and describe()
methods in Airplane
using code similar to the following:
from Vehicle import *
class Airplane(Vehicle):
def __init__(self, name, wingspan, capacity):
super().__init__(name)
self.__wingspan = wingspan
self.__capacity = capacity
def __landing_gear(self, set):
if set:
print("Landing gear down")
else:
print("Landing gear up")
def move(self, distance):
self.__landing_gear(False)
print("Moving")
self.__landing_gear(True)
return distance / self.speed
def describe(self):
return "I am an airplane with a wingspan of {} and capacity {}".format(self.__wingspan, self.__capacity)
In this code, we see that we have included method declarations for both move()
and describe()
that use the exact same method signatures as the ones declared in Vehicle
.
To really understand how this works, let’s look at a quick main()
method that explores how each of these work.
from Vehicle import *
from Airplane import *
class Main:
@staticmethod
def main():
vehicle = Vehicle("Boat")
airplane = Airplane("Plane", 175, 53)
print(vehicle.move(10))
print(airplane.move(10))
print(vehicle.describe())
print(airplane.describe())
# main guard
if __name__ == "__main__":
Main.main()
This code will simply call each method, printing whatever values are returned by the methods themselves. We must also remember that some of the methods may also print information, so we’ll see that output before we see the return value printed.
To run this code, we should see the following output:
In this screenshot, we can see that calling the move()
method on the Vehicle
object just prints the message “Moving”, while calling it on the Airplane
object causes the messages about landing gear to be printed as well. This shows that our Airplane
object is using the code from the overridden move()
method correctly.
In a later part of this chapter, we’ll discuss polymorphism and how overridden methods have a major impact on the functionality of objects stored in that way.
Let’s see if we can do the same for the overridden methods in the Truck
and Car
classes. First, we’ll start with this code for the MotorVehicle
class:
from Vehicle import *
class MotorVehicle(Vehicle):
def __init__(self, name):
super().__init__(name)
self.number_of_wheels = 2
self.engine_volume = 125
def honk_horn(self):
return ""
See if you can complete the code for the Truck
and Car
classes to do the following:
Truck.describe(self)
should return “I’m a big semi truck hauling cargo”Truck.honk_horn(self)
should return “Honk”Car.describe(self)
should return either “I’m a sedan” if it has 4 doors, “I’m a coupe” if it has 2 doors, or “I’m different” if it has any other number of doorsCar.honk_horn(self)
should return “Beep”You’ll also need to write the constructors for each class. Inside the constructor, you should store the parameters provided to the appropriate attributes.
The first test below will determine if your code is structured properly, and the second test will check the functionality of each method.
This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.
Working with inherited classes also gives us an opportunity to learn about how data can be secured in the parent class so that any child class can easily access it, while still making it clear to external classes that these items should not be used.
In Python, we’ve already seen how to implement a “private” variable. Prefixing any attribute or method with two underscores __
will make it difficult to access that item. This is because Python will automatically update the name of that item by adding the class name to it. Private attributes/methods of a super-class (parent) are not directly accessible to a sub-class (child).
In most object oriented languages, there are “middle ground” access modifiers, more protected than “public” but less protected than “private”. One of these is literally called “protected” and noted on a UML class diagram with a #
. Protected generally mean that the attribute or method should be available to child-classes, but not everyone.
Python has no language support for this level of access protection. However, by convention, Python developers add just a single underscore _
in front of an item to indicate to other programmers that the “protected” level of access is intended. It is up to other developers if they want to honor this decision.
So, we can use that method to mark items protected in our parent class, making them more easily identifiable to the child classes, while still making it clear to other classes that these items are internal and should not be accessed directly.
In our UML diagram, we use the hash symbol #
before an item to denote that it should be protected. So, we can update our Vehicle
class to make the speed
attribute protected:
class Vehicle:
def __init__(self, name):
self.__name = name
self._speed = 1.0
@property
def name(self):
return self.__name
def move(self, distance):
print("Moving");
return distance / self._speed;
def describe(self):
return ""
We’ll also have to update the reference to the speed
attribute in the Airplane.move()
method.
Let’s go ahead and update all of the items marked as protected in the UML diagram above in our code. Don’t forget to look for any uses of those attributes as well, since they’ll need to be updated, too. The test below will simply check the structure of the code to make sure we did it correctly.
This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.
Note: this video contains errors in the UML diagram, these errors have been fixed below.
Now that we’ve learned how to build the structure needed for classes to inherit attributes and methods from other classes, let’s explore how we can use those classes in our code.
Earlier, we defined polymorphism as “the condition of occurring in several different forms”1. In code, this allows us to create an object of a child class, and then use it just like an object from its parent class. Let’s look at an example.
from Car import *
from Airplane import *
class Main:
@staticmethod
def main():
plane = Airplane("Plane", 123, 45)
print(plane.name)
print(plane.describe())
print(plane.move(10))
car = Car("Car", 4)
print(car.name)
print(car.describe())
print(car.move(10))
# main guard
if __name__ == "__main__":
Main.main()
In this code, we are instantiating an Airplane
object and a Car
object, but we are treating them just like objects instantiated from the Vehicle
class. This is polymorphism at work! Since both Airplane
and Car
are inheriting from the Vehicle
data type, we can use any methods and access any attributes and properties that are available publicly from the Vehicle
class.
So, when we run this code, we’ll get the following output:
As we can see, even though we are calling methods defined in the Vehicle
class, it is actually using the code from the methods that were defined in the Airplane
and Car
classes, respectively. This is because those methods are overridden in the child classes.
What if we want to use the honk_horn()
method of the Car
object? Could we do that?
car = Car("Car", 4)
print(car.honk_horn())
When we run that code, we’ll see this output:
Yup! That works too. So, even though we are able to treat them as Vehicle
objects, we can still remember that this object is also a Car
object, so we can use those methods as well.
So, when it comes to polymorphism, there are a couple of important rules to remember:
Here’s another example of the power of polymorphism. In this case, we’ll create a list that stores Vehicles
, and fill that list with different types of vehicles.
from Car import *
from Airplane import *
from Truck import *
class Main:
@staticmethod
def main():
vehicles = []
vehicles.append(Airplane("Plane", 123, 45))
vehicles.append(Car("Car", 4))
vehicles.append(Truck("Truck", 157))
for v in vehicles:
print(v.name)
print(v.describe())
print(v.move(10))
# main guard
if __name__ == "__main__":
Main.main()
In this example, we are able to use a for loop to iterate over the objects in the list and call the same methods on each one. This will work as long as each object in the list has methods with the correct names, whether they appear in the object’s class definition or are inherited from a parent class.
So, when we run this program, we’ll see the following output:
Polymorphism is a very powerful tool for object-oriented programmers. Feel free to modify the Main
class open to the left, and then run the code for this example. See if you can create Car
and Truck
objects and store them in the MotorVehicle
data type, then use the honk_horn()
method!
Of course, polymorphism can make things a bit more complicated when it comes to determining exactly what type of object is stored in a variable. Thankfully, Python includes a few easy ways to determine what type of object is really stored in a variable, helping us know what methods and attributes are available.
Let’s go back to the previous example from the last page, where we had placed all of our objects in a list.
from Car import *
from Airplane import *
from Truck import *
class Main:
@staticmethod
def main():
vehicles = []
vehicles.append(Airplane("Plane", 123, 45))
vehicles.append(Car("Car", 4))
vehicles.append(Truck("Truck", 157))
for v in vehicles:
print(v.name)
print(v.describe())
print(v.move(10))
# main guard
if __name__ == "__main__":
Main.main()
What if we’d like to call the honk_horn()
method, but only if the object supports that method? To do that, we’ll need to determine what type of object is stored in the variable. So, we can update the code as shown below:
from Car import *
from Airplane import *
from Truck import *
from MotorVehicle import *
class Main:
@staticmethod
def main():
vehicles = []
vehicles.append(Airplane("Plane", 123, 45))
vehicles.append(Car("Car", 4))
vehicles.append(Truck("Truck", 157))
for v in vehicles:
try:
print(v.honk_horn())
except AttributeError:
print(v.name + " can't honk!")
# main guard
if __name__ == "__main__":
Main.main()
Here, we are doing two very important operations. First, we are using isinstance(v, MotorVehicle)
to determine if the object stored in v
is actually an object that is a MotorVehicle
or one of its child classes. So, for objects created from the Car
and Truck
classes, this operation will return True
since both Car
and Truck
are child classes of MotorVehicle
.
Then, once we’ve determined that we can treat the object v
as a MotorVehicle
, we can call methods and access attributes and properties that are available in the MotorVehicle
class. We don’t have to do anything else in Python for this to work.
It is important to note, however, that if we try to access a method, attribute or property that isn’t available, we’ll get an exception, the AttributeError. So use a Try-Except statement and be prepared to catch an exception if it fails. You may wonder why you don’t “look before you leap” with an IF and isinstance()
. It is not conventional in dynamically typed language to do so.
We can also inspect the type of an object in a more open-ended manner using the type
function, which returns an object representing the argument’s type, printed in the format <class c>
, where c
may be 'int'
, 'str'
, or the name of any other built-in or user-defined type. For example, type(1) == int
would evaluate to True
, and the Python interpreter would print the result of type("Hello")
as <class 'str'>
. However, this method of examining types doesn’t allow us to recover any information about class hierarchies; the type returned is always the most specific applicable type, so we can’t use type
to see whether an instance of a given class can also be treated as an instance of some other class.
Place the code above in the Main
class and see what it does. Can you come up with any other programs that would require us to check the types of objects?
Note: this video contains errors in the UML diagram, these errors have been fixed below.
Another major feature of class inheritance is the ability to define a method in a parent class, but not provide any code that implements that function. In effect, we are saying that all objects of that type must include that method, but it is up to the child classes to provide the code. These methods are called abstract methods, and the classes that contain them are abstract classes. Let’s look at how they work!
In the UML diagram above, we see that the describe()
method in the Vehicle
class is printed in italics. That means that the method should be abstract, without any code provided. To do this in Python, we simply inherit from a special class called ABC
, short for “Abstract Base Class,” and then use the @abstractmethod
decorator:
from abc import ABC, abstractmethod
class Vehicle(ABC):
def __init__(self, name):
self.__name = name
self._speed = 1.0
@property
def name(self):
return self.__name
def move(self, distance):
print("Moving");
return distance / self._speed;
@abstractmethod
def describe(self):
pass
Notice that we must first import both the ABC
class and the @abstractmethod
decorator from a library helpfully called ABC
. Then, we can use ABC
as the parent class of our class, and update each method using the @abstractmethod
decorator before the method, similar to how we’ve already used @staticmethod
in an earlier module.
In addition, since we have declared the method describe()
to be abstract, we can either add some code to that method that can be called using super().describe()
from a child class, or we can simply choose to use the pass
keyword to avoid including any code in the method.
Now, any class that inherits from the Vehicle
class must provide an implementation for the describe()
method. If it does not, that class must also be declared to be abstract. So, for example, in the UML diagram above, we see that the MotorVehicle
class does not include an implementation for describe()
, so we’ll also have to make it abstract.
Of course, that means that we’ll have to inherit from both Vehicle
and ABC
, which we’ll learn about in the next page.
A class with one or more @abstractmethod
declarations cannot be instantiated. In our example, we cannot directly create a Vehicle
.
if __name__ == "main":
v1 = Vehicle()
~$ python3 Vehicle
TypeError: Can't instantiate abstract class a with abstract method describe
~$
This holds true for inherited methods
class a(ABC):
@abstractmethod
def foome(self):
pass
class b(a):
def barme(self):
pass
if __name__ == "__main__":
b1 = b()
running this code results in TypeError: Can't instantiate abstract class a with abstract method foome
.
Finally, we can also build classes that are able to inherit from multiple parent classes. In Python, this is done in a very straightforward way, but it comes with an important caveat.
Before we can really understand the importance of multiple inheritance, we must first discuss the “Diamond Problem.” This is a very common example in object-oriented programming, and it is used to describe one of the common pitfalls for multiple inheritance in programming.
Consider the following code, which defines 4 classes:
class A:
def do_something(self):
pass
class B:
def do_something(self):
print("B")
class C:
def do_something(self):
print("C")
class D(B, C):
pass
In this code, we are trying to inherit from both class B
and C
inside of D
. So, we’ll end up with a class hierarchy diagram that looks something like this:
The problem arises when we try to use the class D
, as in this sample program (ignoring import statements for now):
class Main:
def main():
obj = D()
obj.do_something()
# main guard
if __name__ == "__main__":
Main.main()
In this example, we are calling the do_something()
method on an object instantiated from class D
. However, that method is defined in both B
and C
, which are parents of D
. So, what version of the code should we use?
In this case, Python will look at the parent classes declared in D
, and try to find a method named do_something()
in each class in the order listed. So, since D
inherits from B
and C
, in that order, Python will first determine if there is a method named do_something()
in class B
. Since there is, that is the code that will be executed.
Beyond that, multiple inheritance in Python is very straightforward. We can simply list any number of classes in parentheses as part of the class definition, and then Python will look through those classes in order when looking for the parent methods and attributes. This process also applies to any use of the super()
method as well.
Let’s see if we can update the Vehicle
and MotorVehicle
classes to be abstract, with an abstract definition for the describe()
and horn_honk()
method as well. The test below will simply check the structure of the code to make sure we did it correctly.
This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.
File:Diamond inheritance.svg. (2018, September 25). Wikimedia Commons, the free media repository. Retrieved 02:42, November 4, 2019 from https://commons.wikimedia.org/w/index.php?title=File:Diamond_inheritance.svg&oldid=321823174. ↩︎
Now that we’ve seen how to build classes that can inherit attributes and methods from other classes, let’s work through a simple example program together to see how it all works in practice. The code in this program will be very simple, because the purpose is to explore how we can use the structure of inheritance in our programs.
First, let’s start with a problem statement. In this problem, we are going to build a program that will help us find an object in a toolbox based on several criteria provided from the user. To represent the objects in the toolbox, we’ll use a structure of class inheritance as shown in the UML diagram below:
Right-click image and choose “Open image in new tab” or similar to view larger version
Again this uses the conventional class_name()
syntax for a constructor so make a __init__()
method in its place.
The completed program should be able to perform the following steps:
True
to that method call, then that tool is able to perform that action. The program should print the description of the appropriate tool to the terminal and terminate. In this example, each query will only result in one matching tool, if any.For example, here’s a sample input file that could be provided to this program:
3
AdjustableWrench 170 10 25
CombinationWrench 135 8
CrossCutSaw 350 wood:drywall
Then, if the user inputs the following query:
tighten 150 8
The program will respond with the following output:
CombinationWrench Length: 135 Size: 8
Let’s walk through this program step by step and see how we need to build it.
Tool
ClassFirst, we can start with the Tool
class. Looking at the UML diagram, we see that the describe()
method is in italics, meaning it should be an abstract method. That helps us realize that the entire Tool
class should be abstract. So, we can easily create it and define the constructor and the describe()
method in code:
from abc import ABC, abstractmethod
class Tool(ABC):
def __init__(self):
pass
@abstractmethod
def describe(self):
pass
That’s really it! We’ll need to remember to import both ABC
and abstractmethod
from the abc
library, and then use them to mark this class and the describe()
method as abstract. In many cases, the base class includes very little, if any, content or code. Instead, it simply gives us a shared starting point for the other classes in this program, and defines a single method, describe()
, that each child class must implement.
Wrench
and Saw
ClassesNext, we can go down a level and implement the Wrench
and Saw
classes. Each of these classes contains a single attribute with a getter method. They also each contain an abstract method defining what each type of tool can do. Since neither of these classes implements the describe()
method, even though they inherit from Tool
, they will also be abstract. So, the code for these classes will be very similar to what we already created for the Tool
class:
from Tool import *
from abc import ABC, abstractmethod
class Wrench(Tool, ABC):
def __init__(self, length):
self.__length = length
@property
def length(self):
return self.__length
@abstractmethod
def tighten(self, clearance, size):
pass
from Tool import *
from abc import ABC, abstractmethod
class Saw(Tool, ABC):
def __init__(self, length):
self.__length = length
@property
def length(self):
return self.__length
@abstractmethod
def cut(self, length, material):
pass
As we can see in the code above, these classes are nearly identical, differing only in the name of the class and the method signatures of the different abstract methods.
At this point, we can quickly check our program structure to make sure everything is built correctly so far. Use the assessment below to confirm that your program structure is correct before continuing.
This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.
AdjustableWrench
ClassNext, let’s look at one of the child classes of Wrench
. As we can see in the UML diagram above, this class has both a min_size
and a max_size
attribute that are set through the constructor, as well as getter property methods for each one. So, most of the code for this class is already pretty straight forward, just based on the structure of the class alone.
from Wrench import *
class AdjustableWrench(Wrench):
def __init__(self, length, min_size, max_size):
super().__init__(length)
self.__min_size = min_size
self.__max_size = max_size
@property
def min_size(self):
return self.__min_size
@property
def max_size(self):
return self.__max_size
# other methods go here
So, that just leaves the describe()
and tighten()
methods. Let’s tackle describe()
first. In the example above, we see that the describe()
method seems to just print the name of the class, followed by each attribute’s name and value. So, we can pretty easily implement that method in code:
def describe(self):
return "AdjustableWrench: Length: {} MinSize: {} MaxSize: {}".format(self.length, self.min_size, self.max_size)
Notice that we are using the property getter methods for each attribute. We must do this since length
is private in the Wrench
class, so this is the most straightforward way to access that value.
The tighten()
method should determine whether this wrench is able to tighten the item described. To really understand what we are dealing with, we must understand what an adjustable wrench looks like. Here’s a picture of one from the real world:
The function accepts two parameters: a clearance value, which shows how much room between the item and the surrounding equipment there is, and the size of the item to be tightened itself. So, we know that if our wrench is shorter than the clearance, and supports an item of the given size, we’ll be able to tighten it.
An adjustable wrench has a head that can be adjusted to multiple sizes, so as long as the size given is between the minimum and maximum size our wrench is able to tighten, we can return true. So, to put that into code:
def tighten(self, clearance, size):
return clearance >= self.length and size >= self.min_size and size <= self.max_size
As you may recall from an earlier module, we can directly return the result of a Boolean logic expression, so that makes this method even simpler.
CombinationWrench
and OpenEndWrench
ClassesNow that we’ve written the code for the AdjustableWrench
class, it should be pretty simple to write the code for the other two types of wrenches.
First, a CombinationWrench
, which typically only supports one size of bolt or nut. It typically looks like this.
So, the tighten()
method must simply check the clearance and the size of the item provided against the size of the wrench. Here’s the code for that class:
from Wrench import *
class CombinationWrench(Wrench):
def __init__(self, length, size):
super().__init__(length)
self.__size = size
@property
def size(self):
return self.__size
def describe(self):
return "CombinationWrench Length: {} Size: {}".format(self.length, self.size)
def tighten(self, clearance, size):
return clearance >= self.length and size == self.size
The other type of wrench, an OpenEndWrench
, typically has two heads of different size on either end:
So, it can tighten bolts or nuts of two different sizes. Therefore, the tighten()
method must determine if either size is applicable to the bolt or nut to be tightened. The code for that class is as follows:
from Wrench import *
class OpenEndWrench(Wrench):
def __init__(self, length, size_one, size_two):
super().__init__(length)
self.__size_one = size_one
self.__size_two = size_two
@property
def size_one(self):
return self.__size_one
@property
def size_two(self):
return self.__size_two
def describe(self):
return "CombinationWrench Length: {} SizeOne: {} SizeTwo: {}".format(self.length, self.size_one, self.size_two)
def tighten(self, clearance, size):
return clearance >= self.length and (size == self.size_one or size == self.size_two)
That’s really it! As we can see, while there is quite a bit of code in this program, much of the code is very similar between classes. We’re simply implementing the important bits and pieces of each class, with a slightly different implementation of the describe()
and tighten()
methods in each one.
At this point, we can check our code to confirm that the structure is correct. Use the assessment below to check your program’s structure before continuing.
This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.
CrossCutSaw
ClassThe CrossCutSaw
class is very similar to the classes we created for the different type of wrenches above. The only difference is that it uses a cut()
method to determine whether the saw is able to cut the material described when we call that method.
First, let’s look at the rest of the code for that class. In the constructor, we are given a string that contains a list of materials that can be cut by the saw, separated by colons. So, we’ll need to use the str.split()
method to split that string into a list of strings to be stored in the class’s materials
attribute.
Likewise, since the materials()
property method should return a simple string, we can use the str.join()
method to make a string out of the list, with each element separated by a comma followed by a space. Finally, we can use that to help populate the describe()
method.
from Saw import *
class CrossCutSaw(Saw):
def __init__(self, length, materials):
super().__init__(length)
self.__materials = materials.split(":")
@property
def materials(self):
return ", ".join(self.__materials)
def describe(self):
"CrossCutSaw Length: {} Materials: {}".format(self.length, self.materials)
# additional methods here
It might be tempting to have the CrossCutSaw
class simply accept a list of materials in the constructor, and then return that list in the getMaterials()
method. However, recall that lists are complex data type that are handled using call by reference. So, that leaves this class vulnerable to manipulation from an external code source.
For example, if the Main
class gives a list of materials to CrossCutSaw
via the constructor, we could simply store the reference to that list in our materials
attribute. However, if Main
proceeds to change some of the elements in the list, it would also update the list referenced by this class. Likewise, any code that calls the materials()
method would also get a reference to the same list.
By creating our own list in the constructor, and then only returning a newly formed string each time a class calls the materials()
method, we can protect our data from malicious changes.
An alternative method would be to create a deep copy of the list and store that copy in this class. We haven’t discussed how to do that in this course, but a future course on data structures will cover that process in depth.
The CrossCutSaw
has two more methods that we’ll need to implement: cut()
and find_material()
. The find_material()
method is a private method that allows us to search the list of materials that can be cut by this CrossCutSaw
object, and simply return a Boolean value if the provided material is in the list. So, let’s address that method first.
def __find_material(self, material):
for m in self.__materials:
if m == material:
return True
return False
This method simply iterates through each material in the materials
list, and returns true if it finds a material that exactly matches the material provided as a parameter. We must be careful to refer to the private attribute __materials
and not the property materials
, so we get the actual list and not a string. If it can’t find a match and reaches the end of the list, then the method will return False
.
We can then use this method in our cut()
method to determine whether the given material can be cut by this saw:
def cut(self, length, material):
return length < self.length and self.__find_material(material)
This method will simply return True
if the length of the item to be cut is shorter than the saw and the material of the item is contained in the list of materials that can be cut by this saw. That covers the CrossCutSaw
class.
HackSaw
ClassThe HackSaw
class is very similar to the CrossCutSaw
class. However, instead of having a list of materials that it can cut, a HackSaw
can only cut a single material: metal. So, we can just hard-code that material into the saw’s class, as shown in the code below:
from Saw import *
class HackSaw(Saw):
def __init__(self, length):
super().__init__(length)
def describe(self):
return "HackSaw Length: {} Material: metal".format(self.length)
def cut(self, length, material):
return length < self.length and material == "metal"
That’s all there is to it! At this point, we can check our code to confirm that the structure is correct. Use the assessment below to check your program’s structure before continuing.
This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.
Main
ClassFinally, we need to create a few methods in the Main
class to build the actual logic for our program. Before we build the main()
method, let’s look at the other two methods.
First, the read_input()
method should receive the name of a file as a string, and then return a list of tools that represents the tools specified in the given file. Also, looking at the UML diagram above, that method should be static, since it is underlined. In fact, all of the methods in the Main
class are static, so we can call them directly without instantiating an object using the Main
class.
from Wrench import *
from AdjustableWrench import *
from CombinationWrench import *
from OpenEndWrench import *
from Saw import *
from HackSaw import *
from CrossCutSaw import *
import sys
class Main:
# other methods go here
@staticmethod
def read_input(filename):
try:
with open(filename) as reader:
num_tools = int(reader.readline())
tools = []
for i in range(0, num_tools):
line = reader.readline().split(" ")
length = int(line[1])
if line[0] == "AdjustableWrench":
min_size = int(line[2])
max_size = int(line[3])
tools.append(AdjustableWrench(length, min_size, max_size))
elif line[0] == "OpenEndWrench":
size_one = int(line[2])
size_two = int(line[3])
tools.append(OpenEndWrench(length, size_one, size_two))
elif line[0] == "CombinationWrench":
size = int(line[2])
tools.append(CombinationWrench(length, size))
elif line[0] == "CrossCutSaw":
tools.append(CrossCutSaw(length, line[2]))
elif line[0] == "HackSaw":
tools.append(HackSaw(length))
else:
raise Exception("Unknown Tool: " + line[0])
return tools
except Exception:
print("Invalid Input")
return []
# main guard
if __name__ == "__main__":
Main.main(sys.argv)
The read_input()
method looks quite complex, but it is actually really simple. First, it tries to open the file provided using a With statement inside of a Try-Except statement. Then, inside of that statement, it will read the first line of input and create the list of tools. Then, using a For loop, it will read each line of input. Those lines can immediately be split into a list of tokens using the str.split()
method. Then, we simply use a bunch of If-Else-If-Else statements to determine which type of tool must be created based on the first token in the input. Then, we can use subsequent tokens as input to the constructors for each class, converting inputs to integers as needed.
If we can’t find a matching tool, we can simply throw a new exception with a helpful error message.
Finally, since we simply need to catch any possible exception, we’ll just add a catch statement for the generic exception and print the “Invalid Input” message before returning an empty list of tools.
Once we have a list of tools, we can also write the find_tool()
method that will search the list of tools for a tool that can do the job. We could do so using this code:
@staticmethod
def find_tool(tools, query):
query_parts = query.split(" ")
if query_parts[0] == "tighten":
clearance = int(query_parts[1])
size = int(query_parts[2])
for t in tools:
if isinstance(t, Wrench):
if t.tighten(clearance, size):
return t
return ??
elif query_parts[0] == "cut":
length = int(query_parts[1])
for t in tools:
if isinstance(t, Saw):
if t.cut(length, query_parts[2]):
return t
return ??
else:
return ??
This method is also a bit complex, but upon closer inspection it should be pretty straightforward. We simply parse the query into individual tokens. Then, we use the first token to determine if we are looking for a wrench or a saw. Next, we iterate through the entire list of tools, and inside of the For loop, we check to see if the current tool is either a wrench or a saw, whichever type we are looking for. If it is, we then call the appropriate method. If that method returns True
, we know that the tool can perform the requested task, so we can just return it right there!
What if we get to the end and can’t find a tool that matches? This method still needs to return an object of the Tool
type. For lists, we’ve been returning an empty list to show that the method was unsuccessful. Is there such as thing as an “empty object”?
It turns out there is! Python uses a special keyword called None
to represent an empty object. So, we can just return None
anywhere we aren’t sure what to return, and we’ll use that value in our main()
method to determine whether we found a tool or not. Python will return None
by default if a function ends without specifically returning a value, but we’ll write out return None
in the relevant places here to make it explicit.
@staticmethod
def find_tool(tools, query):
query_parts = query.split(" ")
if query_parts[0] == "tighten":
clearance = int(query_parts[1])
size = int(query_parts[2])
for t in tools:
if isinstance(t, Wrench):
if t.tighten(clearance, size):
return t
return None
elif query_parts[0] == "cut":
length = int(query_parts[1])
for t in tools:
if isinstance(t, Saw):
if t.cut(length, query_parts[2]):
return t
return None
else:
return None
Finally, we can simply write the main()
method:
@staticmethod
def main(args):
if len(args) != 2:
print("Invalid Input")
return
tools = Main.read_input(args[1])
if len(tools) == 0:
return
try:
with sys.stdin as reader:
query = reader.readline()
t = Main.find_tool(tools, query)
if t is not None:
print(t.describe())
else:
print("Invalid Tool")
except Exception:
print("Invalid Tool")
return
In the main()
method, we check to make sure that we’ve received exactly one command-line argument (in addition to the name of the program, which will be the first argument). If so, we pass that argument to the read_input()
method to read from the input file and produce a list of tools. If that list is empty, we know that we failed to read the input file correctly, so we should simply return.
If the list is populated, then we must read input from the terminal. So, we’ll use a With and Try-Except statement to read a user’s query, and then pass that query to the find_tool()
method along with the list of tools.
If the find_tool()
method returns anything other than None
, we know that we found a tool and should print the tool’s description to the terminal. Otherwise, we can do nothing since we are at the end of the program.
Of course, if we encounter any exceptions while reading input from the terminal, we can simply print “Invalid Tool” and return.
There we go! This is a very simple program, but it helps demonstrate the power of using inheritance in our programs to represent real-world objects that are closely related to each other.
We can use the first assessment below to check the structure of our entire program, making sure it matches the UML diagram given above. Once our program passes that assessment, we can use the second assessment to check the functionality of our program. For simplicity, the functionality tests will not use the main()
method itself, but it will check the other two methods defined in the Main
class as well as several other methods of the various classes.
This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.