COMPSCI 107 Computer Science Fundamentals Lecture 09 – Classes
Learning outcomes At the end of this lecture, students should be able to: Define a class that overrides the default behaviour for standard operations COMPSCI 107 - Computer Science Fundamentals
Example: a Fraction class Consider writing a class to represent a fraction in Python Create a fraction Add, subtract, multiply, divide two fractions Display a text representation of a fraction numerator 1 / 2 denominator
Example: a Fraction class class Fraction: pass f = Fraction() f.num = 1 f.den = 2 print('{} / {}'.format(f.num, f.den))
Example: a Fraction class Placing the definition of the Fraction class in a separate file helps to “abstract away” the details of the class implementation from the client code Fraction.py main.py class Fraction: pass from Fraction import Fraction f = Fraction()
Example Fraction object (aka “instance” of the Fraction class) f
Example 1 num den 2 f Fraction object (aka “instance” of the Fraction class) num 1 den 2 f
Visualising Fraction objects We could create several Fraction objects as follows: f1 = Fraction(1, 2) f2 = Fraction(3, 4) f3 = Fraction(7, 8) 1 numerator 2 denominator f1 3 numerator f2 4 denominator f3 7 numerator 8 denominator
Initialiser for the Fraction class All classes must have an initialiser The initialiser for the Fraction class should store the numerator and the denominator Fraction.py class Fraction: def __init__(self, top, bottom): self.numerator = top self.denominator = bottom
Using the Fraction class So far we can create a Fraction object: We can access the state variables directly Although it is considered not good practice to do so What else can we do with Fraction objects? Nothing yet... we need to write the methods! main.py from Fraction import Fraction f1 = Fraction(3, 4) main.py print(f1.numerator) print(f1.denominator)
Using the Fraction class Why is accessing the state variables directly not advised? from Fraction import Fraction f1 = Fraction(3, 4) print(f1.numerator) print(f1.denominator) f1.denominator = 0 This shouldn’t be allowed – but currently we can’t prevent people using our Fraction class like this. What can we do?
Hiding the instance variables To prevent direct modification of the data fields (instance variables), we can stop the client (the user of the class) from accessing them directly This is known as data hiding, which can be done by defining private data fields in a class In Python, the private data fields are defined with two leading underscore characters You can also define private methods in the same way (although this is not something we will do) class Fraction: def __init__(self, top, bottom): self.__numerator = top self.__denominator = bottom
Accessing private data fields Private data fields can be accessed by code within the class definition, but they cannot be accessed by code outside the class (client code) To make a private data field accessible to the client, provide a method to return its value To enable a private data field to be modifiable, provide a method to set its value f1 = Fraction(3, 4) print(f1.__numerator) Traceback (most recent call last): File "lecture.py", line 5, in <module> print(f1.__numerator) AttributeError: 'Fraction' object has no attribute '__numerator'
Accessor and mutator methods A “get” method is referred to as an accessor method A “set” method is referred to as a mutator method def get_numerator(self): return self.__numerator def set_numerator(self, top): self.__numerator = top Example: accessing the private data fields through accessor and mutator methods from Fraction import Fraction f1 = Fraction(1, 2) print(f1.get_numerator()) f1.set_numerator(12) 1 12
Overriding default behaviour All classes get a number of special methods provided by default including methods for creating a text-based representation of the object and methods for comparing two objects of the class type But these default versions are not very useful. We should define our own that make more sense for our class. f1 = Fraction(1, 2) f2 = Fraction(1, 2) print(f1) print(f1 == f2) Calls a special function “__str__” <Fraction.Fraction object at 0x025E7410> False Calls a special function “__eq__”
The __str__ method You should define a __str__ method in your class This should return a “nicely-formatted” version of the object class Fraction: def __init__(self, top, bottom): self.__numerator = top self.__denominator = bottom .... def __str__(self): return '(' + str(self.__numerator) + '/' + str(self.__denominator) + ')' f1 = Fraction(1, 2) print(f1) (1/2)
The __eq__ method The __eq__ method will be called automatically whenever you use “==” to compare two instances of your class the default behaviour just compares the references class Fraction: def __init__(self, top, bottom): self.__numerator = top self.__denominator = bottom def __eq__(self, other): return self.__numerator * other.__denominator == other.__numerator * self.__denominator f1 = Fraction(1, 2) f2 = Fraction(3, 4) f3 = Fraction(3, 6) print(f1 == f2) print(f1 == f3) False True
The __repr__ method There is one more special method that you should define The __repr__ method should return “a string that unambiguously describes the object” Ideally, the representation should be an expression that could be used to create the object class Fraction: def __init__(self, top, bottom): self.__numerator = top self.__denominator = bottom .... def __repr__(self): return 'Fraction(' + str(self.__numerator) + ', ' + str(self.__denominator) + ')' If you don’t define __str__ then by default it will call __repr__, but otherwise __str__ is used for printing. In this example, we call __repr__ explicitly using repr() f1 = Fraction(1, 2) print(repr(f1)) Fraction(1, 2)
Overloading operators Python operators work for built-in classes But the same operator behaves differently with different types For example, consider “+” Performs arithmetic between two numbers Merges two lists Concatenates two strings Operator overloading is when one operator can perform different functions depending on the context
The __add__ method Another special method is __add__ (5/6) This is called automatically when the “+” operator is used In other words, f1 + f2 gets translated into f1.__add__(f2) class Fraction: def __init__(self, top, bottom): self.__numerator = top self.__denominator = bottom def __add__(self, other): new_num = self.__numerator * other.__denominator + self.__denominator * other.__numerator new_den = self.__denominator * other.__denominator return Fraction(new_num, new_den) f1 = Fraction(1, 2) f2 = Fraction(1, 3) result = f1 + f2 print(result) (5/6)
Improving the __eq__ method Recall the __eq__ method in our Fraction class: What happens here? def __eq__(self, other): return self.__numerator * other.__denominator == other.__numerator * self.__denominator x = Fraction(2, 3) y = Fraction(1, 3) z = y + y print(x == z) print(x is z) w = x + y print(w == 1)
Improving the __eq__ method Recall the __eq__ method in our Fraction class: What happens here? def __eq__(self, other): return self.__numerator * other.__denominator == other.__numerator * self.__denominator x = Fraction(2, 3) y = Fraction(1, 3) z = y + y print(x == z) print(x is z) w = x + y print(w == 1)
Improving the __eq__ method Recall the __eq__ method in our Fraction class: What happens here? def __eq__(self, other): return self.__numerator * other.__denominator == other.__numerator * self.__denominator x = Fraction(2, 3) y = Fraction(1, 3) z = y + y print(x == z) print(x is z) w = x + y print(w == 1) True False Traceback (most recent call last): File "lecture.py", line 13, in <module> print(w == 1) File "Fraction.py", line 19, in __eq__ return self.__numerator * ... AttributeError: 'int' object has no attribute '_Fraction__denominator'
Improving the __eq__ method To fix this, we need to check if the type of “other” is a Fraction object But maybe we would like to be able to compare Fraction objects to integers? we could extend this so that if the type of “other” is an int, then we perform an appropriate comparison. def __eq__(self, other): if not isinstance(other, Fraction): return False return self.__numerator * other.__denominator == other.__numerator * self.__denominator
Simplifying fractions There is no need to store numerators or denominators larger than required Simplifying fractions: calculate the greatest common divisor(GCD) of the numerator and denominator (i.e. the common divisors of 12 and 30 are 1, 2, 3 and 6) divide both numerator and denominator by GCD 12 / 30 is the same as 2 / 5
Calculating the GCD Fortunately, Euclid came up with a nice algorithm for computing this around 300BC Given two number, n and m, find the number k such that k is the largest number the evenly divides both n and m: Notice also that there is no “self” as the first input to this method. We will not be calling this method on an object – this is an example of a static method (it is not called on an object of the class) class Fraction: .... def __gcd(m, n): while m % n != 0: old_m = m old_n = n m = old_n n = old_m % old_n return n By defining this method with a leading “__”, it makes it private to the Fraction class. Client code (that uses the Fraction class) cannot directly call the gcd method (it is only used by the Fraction class itself to reduce fractions)
Improving the constructor Now we can improve the constructor so that it always represents a fraction using the “lowest terms” (reduced) form: class Fraction: def __init__(self, top, bottom): common = Fraction.__gcd(top, bottom) self.__numerator = top // common self.__denominator = bottom // common Notice how static methods are called using the name of the class (and not a particular instance of the class)
Improving the constructor Example from Fraction import Fraction x = Fraction(2,3) y = Fraction(3,4) z = x + y print(z) w = z + Fraction(5, 12) print(w) print(w + Fraction(1,6) == Fraction(2,1)) (17/12) (11/6) True
Other standard Python operators Many standard operators and funtions: https://docs.python.org/3/library/operator.html Common Arithmetic operators object.__add__(self, other) object.__sub__(self, other) object.__mul__(self, other) object.__floordiv__(self, other) object.__truediv__(self, other) Common Relational operators object.__lt__(self, other) object.__le__(self, other) object.__eq__(self, other) object.__ne__(self, other) object.__gt__(self, other) object.__ge__(self, other)
Summary A class is a template, a blueprint and a data type for objects A class defines the data fields of objects, and provides an initialiser for initialising objects and other methods for manipulating data The initialiser is always called __init__() The first input parameter is always “self”, and this refers to the object being constructed The data fields in the class should be hidden to prevent data tampering and to make the class easy to maintain This is achieved by prefixing the field name with __ We can override the default methods in a class definition Such as __repr__ and __str__ and __eq__