Part 9

# 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)``````
Sample output

(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()``````
Sample output

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

self.completed_courses.append(course)``````
``````student1 = Student("Sally Student")
student2 = Student("Sassy Student")

print(student1.completed_courses)
print(student2.completed_courses)``````
Sample output

['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

self.completed_courses.append(course)``````
``````student1 = Student("Sally Student")
student2 = Student("Sassy Student")

print(student1.completed_courses)
print(student2.completed_courses)``````
Sample output

['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.