More examples with classes
The following example consists of two classes. The class Point
is a model for a point in two-dimensional space. The class Line
is a model for a line segment between two points. The code below is commented; please read the comments in order to understand how the classes work.
import math
class Point:
""" The class represents a point in two-dimensional space """
def __init__(self, x: float, y: float):
# These attributes are public because any value is acceptable for x and y
self.x = x
self.y = y
# This class method returns a new Point at origo (0, 0)
# It is possible to return a new instance of the class from within the class
@classmethod
def origo(cls):
return Point(0, 0)
# This class method creates a new Point based on an existing Point
# The original Point can be mirrored on either or both of the x and y axes
# For example, the Point (1, 3) mirrored on the x-axis is (1, -3)
@classmethod
def mirrored(cls, point: "Point", mirror_x: bool, mirror_y: bool):
x = point.x
y = point.y
if mirror_x:
y = -y
if mirror_y:
x = -x
return Point(x, y)
def __str__(self):
return f"({self.x}, {self.y})"
class Line:
""" The class represents a line segment in two-dimensional space """
def __init__(self, beginning: Point, end: Point):
# These attributes are public because any two Points are acceptable
self.beginning = beginning
self.end = end
# This method uses the Pythagorean theorem to calculate the length of the line segment
def length(self):
sum_of_squares = (self.end.x - self.beginning.x) ** 2 + (self.end.y - self.beginning.y) ** 2
return math.sqrt(sum_of_squares)
# This method returns the Point in the middle of the line segment
def centre_point(self):
centre_x = (self.beginning.x + self.end.x) / 2
centre_y = (self.beginning.y + self.end.y) / 2
return Point(centre_x, centre_y)
def __str__(self):
return f"{self.beginning} ... {self.end}"
point = Point(1,3)
print(point)
origo = Point.origo()
print(origo)
point2 = Point.mirrored(point, True, True)
print(point2)
line = Line(point, point2)
print(line.length())
print(line.centre_point())
print(line)
(1, 3) (0, 0) (-1, -3) 6.324555320336759 (0.0, 0.0) (1, 3) ... (-1, -3)
Default values of parameters
In Python programming you can generally set a default value for any parameter. Default values can be used in both functions and methods.
If a parameter has a default value, you do not have to include a value as an argument when calling the function. If an argument is given, the default value is ignored. If not, the default value is used.
Default values are often used in constructors. If it can be expected that not all information is available when an object is created, it is better to include a default value in the definition of the constructor method than to force the client to take care of the issue. This makes using the class easier from the client's point of view, but it also ensures the integrity of the object. For instance, with a set default value we can be sure that an "empty" value is always the same, unless the client specifically wants to supply something different. If a default value is not set, it is up to the client to provide an "empty" value. This could be, for example, an empty string ""
, the special empty object None
, or the string "not set"
.
Let's have a look at yet another class representing a student. When creating a new Student object the client must provide a name and a student number. The student number is private and should not be changed later. Additionally, a Student object has attributes for study credits and notes, which have default values set in the constructor. New values can be passed as arguments to the constructor, but they can also be left out so that the default values are used instead. Please have a look at the comments in the code to better understand what each method does.
class Student:
""" This class models a student """
def __init__(self, name: str, student_number: str, credits: int = 0, notes: str = ""):
# calling the setter method for the name attribute
self.name = name
if len(student_number) < 5:
raise ValueError("A student number should have at least five characters")
self.__student_number = student_number
# calling the setter method for the credits attribute
self.credits = credits
self.__notes = notes
@property
def name(self):
return self.__name
@name.setter
def name(self, name):
if name != "":
self.__name = name
else:
raise ValueError("The name cannot be an empty string")
@property
def student_number(self):
return self.__student_number
@property
def credits(self):
return self.__credits
@credits.setter
def credits(self, op):
if op >= 0:
self.__credits = op
else:
raise ValueError("The number of study credits cannot be below zero")
@property
def notes(self):
return self.__notes
@notes.setter
def notes(self, notes):
self.__notes = notes
def summary(self):
print(f"Student {self.__name} ({self.student_number}):")
print(f"- credits: {self.__credits}")
print(f"- notes: {self.notes}")
# Passing only the name and the student number as arguments to the constructor
student1 = Student("Sally Student", "12345")
student1.summary()
# Passing the name, the student number and the number of study credits
student2 = Student("Sassy Student", "54321", 25)
student2.summary()
# Passing values for all the parameters
student3 = Student("Saul Student", "99999", 140, "extra time in exam")
student3.summary()
# Passing a value for notes, but not for study credits
# NB: the parameter must be named now that the arguments are not in order
student4 = Student("Sandy Student", "98765", notes="absent in academic year 20-21")
student4.summary()
Student Sally Student (12345):
- credits: 0
- notes:
Student Sassy Student (54321):
- credits: 25
- notes:
Student Saul Student (99999):
- credits: 140
- notes: extra time in exam
Student Sandy Student (98765):
- credits: 0
- notes: absent in academic year 20-21
NB: there is no setter method for the attribute student_number
as the student number is not supposed to change.
There is one rather significant snag when using default values for parameters. The following example modelling yet another kind of student will shed more light on this:
class Student:
def __init__(self, name, completed_courses=[]):
self.name = name
self.completed_courses = completed_courses
def add_course(self, course):
self.completed_courses.append(course)
student1 = Student("Sally Student")
student2 = Student("Sassy Student")
student1.add_course("ItP")
student1.add_course("ACiP")
print(student1.completed_courses)
print(student2.completed_courses)
['ItP', 'ACiP'] ['ItP', 'ACiP']
Adding completed courses to Sally's list also adds those courses to Sassy's list. In fact, these two are the exact same list, as Python reuses the reference stored in the default value. Creating the two new Student objects in the above example is equivalent to the following:
courses = []
student1 = Student("Sally Student", courses)
student2 = Student("Sassy Student", courses)
The default values of parameters should never be instances of more complicated, mutable data structures, such as lists. The problem can be circumvented by making the following changes to the constructor of the Student
class:
class Student:
def __init__(self, name, completed_courses=None):
self.name = name
if completed_courses is None:
self.completed_courses = []
else:
self.completed_courses = completed_courses
def add_course(self, course):
self.completed_courses.append(course)
student1 = Student("Sally Student")
student2 = Student("Sassy Student")
student1.add_course("ItP")
student1.add_course("ACiP")
print(student1.completed_courses)
print(student2.completed_courses)
['ItP', 'ACiP'] []
The Grand Finale
Even though the following exercise finishes off this part of the material, the techniques required to solve it were all covered already in the section named objects as attributes. Specifically, you are not required to use the @property
decorator or default values for parameters in this exercise. This exercise is very similar to the exercises a box of presents and the shortest person in the room.
Item, Suitcase and Cargo hold
/
In this series of exercises you will create the classes Item
, Suitcase
and Cargo Hold
, which will let you further practice working on objects which contain references to other objects.
Item
Please create a class named Item
which is used to create items of different kinds. Each item has a name and a weight (in kilograms).
You can use the following code to test your class:
book = Item("ABC Book", 2)
phone = Item("Nokia 3210", 1)
print("Name of the book:", book.name())
print("Weight of the book:", book.weight())
print("Book:", book)
print("Phone:", phone)
Your program should print out this:
Name of the book: ABC Book Weight of the book: 2 Book: ABC Book (2 kg) Phone: Nokia 3210 (1 kg)
An Item
should provide the methods weight
and name
, which return the values stored in those attributes.
The name and weight should be encapsulated within the class. The following code should not work:
book = Item("ABC Book", 2)
book.weight = 10
Suitcase
Please write a class named Suitcase
. You should be able to pack items into a suitcase. A suitcase also has a maximum combined weight for the items stored within.
Your class should contains the following members:
- a constructor which takes the maximum weight as an argument
- a method named
add_item
which adds the item given as an argument to the suitcase. The method has no return value. - a
__str__
method which returns a string in the format "x items (y kg)"
The class should make sure that the combined weight of the items stored within any Suitcase
does not exceed the maximum weight set for that instance. If the maximum weight would be exceeded when the add_item
method is called, the new item should not be added to the suitcase.
Your class should work as follows:
book = Item("ABC Book", 2)
phone = Item("Nokia 3210", 1)
brick = Item("Brick", 4)
suitcase = Suitcase(5)
print(suitcase)
suitcase.add_item(book)
print(suitcase)
suitcase.add_item(phone)
print(suitcase)
suitcase.add_item(brick)
print(suitcase)
Executing the above should print out
0 items (0 kg) 1 items (2 kg) 2 items (3 kg) 2 items (3 kg)
Mind your language
The notification "1 items" is not very grammatical. Instead, it should say "1 item". Please make the required changes to your __str__
method.
The previous example should now print out
0 items (0 kg) 1 item (2 kg) 2 items (3 kg) 2 items (3 kg)
All the items
Please add the following methods to your Suitcase
class definition:
print_items
prints out all the items stored in the suitcaseweight
returns an integer number representing the combined weight of all the items stored in the suitcase
Your class should now work with the following program:
book = Item("ABC Book", 2)
phone = Item("Nokia 3210", 1)
brick = Item("Brick", 4)
suitcase = Suitcase(10)
suitcase.add_item(book)
suitcase.add_item(phone)
suitcase.add_item(brick)
print("The suitcase contains the following items:")
suitcase.print_items()
combined_weight = suitcase.weight()
print(f"Combined weight: {combined_weight} kg")
Executing the above program should print out this:
The suitcase contains the following items: ABC Book (2 kg) Nokia 3210 (1 kg) Brick (4 kg) Combined weight: 7 kg
If you have implemented your Suitcase
class with more than two instance variables, please make the required changes so that each instance has only two data attributes: the maximum weight and a list of items within.
The heaviest item
Please add a new method to your Suitcase
class: heaviest_item
should return the Item
which is the heaviest. If there are two or more items with the same, heaviest weight, the method may return any one of these. The method should return a reference to an object. If the suitcase is empty, the method should return None
.
Your class should now work with the following program:
book = Item("ABC Book", 2)
phone = Item("Nokia 3210", 1)
brick = Item("Brick", 4)
suitcase = Suitcase(10)
suitcase.add_item(book)
suitcase.add_item(phone)
suitcase.add_item(brick)
heaviest = suitcase.heaviest_item()
print(f"The heaviest item: {heaviest}")
Executing the above program should print out this:
The heaviest item: Brick (4 kg)
Cargo hold
Please write a class named CargoHold
with the following methods:
- a constructor which takes the maximum weight as an argument
- a method named
add_suitcase
which adds the suitcase given as an argument to the cargo hold - a
__str__
method which returns a string in the format "x suitcases, space for y kg"
The class should make sure that the combined weight of the items stored within any CargoHold
does not exceed the maximum weight set for that instance. If the maximum weight would be exceeded when the add_suitcase
method is called, the new suitcase should not be added to the cargo hold.
Your class should now work with the following program:
cargo_hold = CargoHold(1000)
print(cargo_hold)
book = Item("ABC Book", 2)
phone = Item("Nokia 3210", 1)
brick = Item("Brick", 4)
adas_suitcase = Suitcase(10)
adas_suitcase.add_item(book)
adas_suitcase.add_item(phone)
peters_suitcase = Suitcase(10)
peters_suitcase.add_item(brick)
cargo_hold.add_suitcase(adas_suitcase)
print(cargo_hold)
cargo_hold.add_suitcase(peters_suitcase)
print(cargo_hold)
0 suitcases, space for 1000 kg 1 suitcase, space for 997 kg 2 suitcases, space for 993 kg
The contents of the cargo hold
Please add a method named print_items
to your CargoHold
class. It should print out all the items in all the suitcases within the cargo hold.
Your class should now work with the following program:
book = Item("ABC Book", 2)
phone = Item("Nokia 3210", 1)
brick = Item("Brick", 4)
adas_suitcase = Suitcase(10)
adas_suitcase.add_item(book)
adas_suitcase.add_item(phone)
peters_suitcase = Suitcase(10)
peters_suitcase.add_item(brick)
cargo_hold = CargoHold(1000)
cargo_hold.add_suitcase(adas_suitcase)
cargo_hold.add_suitcase(peters_suitcase)
print("The suitcases in the cargo hold contain the following items:")
cargo_hold.print_items()
Executing the above program should print out this:
The suitcases in the cargo hold contain the following items: ABC Book (2 kg) Nokia 3210 (1 kg) Brick (4 kg)
Please respond to a quick questionnaire on this week's materials.
Log in to view the exercise
You can check your current points from the blue blob in the bottom-right corner of the page.