Functions and Classes in Python

Classes

Overview:

  • Teaching: 10 min
  • Exercises: 30 min

Questions

  • What is a class?
  • How do I define a class?
  • What should a class contain?

Objectives

  • Know how to define an class

What is a class?

Classes allow you to define how to package data with functions to create objects. An object is an instance of a class, which contains its own data, and its own copy of functions that can operate on that data.

You use classes to define objects that represent the concepts and things that your program will work with. For example, if your program managed exam results of students, then you may create one class that represents an Exam, and another that represents a Student.

In [1]:
class Exam:
    def __init__(self, max_score=100):
        self._max_score = max_score
        self._actual_score = 0
        
    def percent(self):
        return 100.0 * self._actual_score / self._max_score
    
    def setResult(self, score):
        if (score < 0):
            self._actual_score = 0
        elif (score > self._max_score):
            self._actual_score = self._max_score
        else:
            self._actual_score = score
    
    def grade(self):
        if (self._actual_score == 0):
            return "U"
        elif (self.percent() > 90.0):
            return "A"
        elif (self.percent() > 80.0):
            return "B"
        elif (self.percent() > 70.0):
            return "C"
        else:
            return "F"
In [2]:
class Student:
    def __init__(self):
        self._exams = {}
    
    def addExam(self, name, exam):
        self._exams[name] = exam
        
    def addResult(self, name, score):
        self._exams[name].setResult(score)
    
    def result(self, exam):
        return self._exams[exam].percent()
    
    def grade(self, exam):
        return self._exams[exam].grade()
    
    def grades(self):
        g = {}
        for exam in self._exams.keys():
            g[exam] = self.grade(exam)
        return g

We can now create a student, and give them a set of exams that they need to complete.

In [3]:
s = Student()
In [4]:
s.addExam( "maths", Exam(20) )
In [5]:
s.addExam( "chemistry", Exam(75) )

At this point, the student has not completed any exams, so the grades are all 'U'

In [6]:
s.grades()
Out[6]:
{'maths': 'U', 'chemistry': 'U'}

However, we can now add the results...

In [7]:
s.addResult("maths", 15)
In [8]:
s.addResult("chemistry", 62)
In [9]:
s.grades()
Out[9]:
{'maths': 'C', 'chemistry': 'B'}

Programming with classes makes the code easier to read, as the code more closely represents the concepts that make up the program. For example, here we have a class that represents a full school of students.

In [10]:
class School:
    def __init__(self):
        self._students = {}
        self._exams = []

    def addStudent(self, name):
        self._students[name] = Student()

    def addExam(self, exam, max_score):
        self._exams.append(exam)
        
        for key in self._students.keys():
            self._students[key].addExam(exam, Exam(max_score))
    
    def addResult(self, name, exam, score):
        self._students[name].addResult(exam, score)
        
    def grades(self):
        g = {}
        for name in self._students.keys():
            g[name] = self._students[name].grades()
        return g

We can now create a whole school of students and manage the exams and results for all of them with some reasonably readable code :-)

In [11]:
school = School()
In [12]:
school.addStudent("Charlie")
In [13]:
school.addStudent("Matt")
In [14]:
school.addStudent("James")
In [15]:
school.addExam( "maths", 20 )
In [16]:
school.addExam( "physics", 50 )
In [17]:
school.addExam( "english literature", 30 )
In [18]:
school.grades()
Out[18]:
{'Charlie': {'maths': 'U', 'physics': 'U', 'english literature': 'U'},
 'Matt': {'maths': 'U', 'physics': 'U', 'english literature': 'U'},
 'James': {'maths': 'U', 'physics': 'U', 'english literature': 'U'}}

We can now add in the results of the exams, which have been returned to us by the exam markers...

In [19]:
englit_results = { "Charlie" : 10, "Matt" : 25, "James" : 3 }
In [20]:
phys_results = { "Matt" : 48, "James" : 3 }
In [21]:
maths_results = { "James" : 20, "Matt" : 18, "Charlie" : 4 }

Indeed, we will do this by using a function...

In [22]:
def add_results(school, exam, results):
    for student in results.keys():
        school.addResult(student, exam, results[student])
In [23]:
add_results(school, "english literature", englit_results)
In [24]:
add_results(school, "physics", phys_results)
In [25]:
add_results(school, "maths", maths_results)
In [26]:
school.grades()
Out[26]:
{'Charlie': {'maths': 'F', 'physics': 'U', 'english literature': 'F'},
 'Matt': {'maths': 'B', 'physics': 'A', 'english literature': 'B'},
 'James': {'maths': 'A', 'physics': 'F', 'english literature': 'F'}}

Exercises

1

Starting with the Morse class from the last section, modify the class to add in a decode function that converts Morse code back to English. Check that this class works by seeing if m.decode( m.encode(message) ) == message.lower().

Solution

2

Using the School class, together with the code needed to populate an object of that class with students and exam results, edit the School class to add in the following functions:

  • .resits() : this should return the list of exams that each student should resit if they get a "F" or "U" grade.
  • .prizeStudent() : this should return the name of the student who scored the highest average percent across all of the exams.
  • .reviseCourse(threshold) : this should return the name of the exam that gets the lowest average score across all students, if the average score is below threshold.

Use these functions to find out which students need to resit which exams, which student should be awarded the annual school prize, and which courses should be revised as the average mark is less than 50%.

Solution

Key Points:

  • Classes combine data and functions into useful core functioanlity
  • Classes can contain instances other other classes
  • Building modular code in this way helps to produce readable, maintainable and testable code.