This site is from a past semester! The current version will be here when the new semester starts.

Week 10 [Mon, Mar 20th] - Programming Topics

Object-Oriented Programming

Guidance for the item(s) below:

Similar to last week, we strongly recommend you to watch all pre-recorded lecture videos allocated to this week. Furthermore, Learn this week's OOP topic (W10.1) before starting the topic below, as the two are highly interlinked. The same lecture video is given below for your convenience.

Video

Inheritance

You can make a class from another class. If you do, it is as if the already has all the attributes and methods of the .

Syntax:

class ChildClassName(ParentClassName):
  # statements of the class

Consider the Person class below:

class Person:
  def __init__(self, name):
    self.name = name

  def print_info(self):
    print('My name is', self.name)

amy = Person('Amy')
amy.print_info()
 → 

My name is Amy

The Teacher class below inherits from the Person class given above.

class Teacher(Person):

  def teach(self):
    print(self.name, 'is teaching')

dan = Teacher('Dan')
dan.print_info()
dan.teach()
print('the name is', dan.name)
 → 

My name is Dan
Dan is teaching
the name is Dan

Observe how,

  • a Teacher object can use the print_info() method defined in the parent class.
  • the statement dan = Teacher('Dan') invokes the __init__() method defined in the parent class too.
  • the statement print('the name is', dan.name) is accessing the attribute name from a Teacher object although the attribute is defined in the parent class.
  • the method teach accesses the attribute name using self.name although the attribute is defined in the parent class.

Exercise: Add Fish Class

Exercise : Add Fish Class

Add a Fish class that inherits from the Animal class so that the code below produces the given output.

class Animal:
  def __init__(self, name):
    self.name = name
    
  def info(self):
    print("I'm a", self.name)
    
# ADD YOUR CODE HERE
    
tuna = Fish('Tuna')
tuna.info()
tuna.move()
guppy = Fish('Guppy')
guppy.info()
guppy.move()
 → 

I'm a Tuna
I'm swimming
I'm a Guppy
I'm swimming

Partial solution



A child class can override a method defined in the parent class. That way, a child object can change a behavior defined in the parent class.

Note how the Student class below overrides the print_info() method of the parent class Person.

class Person:
  def __init__(self, name):
    self.name = name

  def print_info(self):
    print(self.name, 'is a person')
adam = Person('Adam')
adam.print_info()
ben = Student('Ben')
ben.print_info()
 → 

class Student(Person):

  def print_info(self):
    print(self.name, 'is a student')




Adam is a person
Ben is a student

When overriding methods, you can reuse the parent's definition of the same method using the super(). prefix.

class Person:
  def __init__(self, name):
    self.name = name

Given that Person class has the initializer method given above, the following two versions of the Student class are equivalent.

(a) Override without reusing parent's method

class Student(Person):
  def __init__(self, name, matric):
    self.name = name
    self.matric = matric

(b) Override but reuse parent's method (preferred)

class Student(Person):
  def __init__(self, name, matric):
    super().__init__(name) # reuse parent's method
    self.matric = matric

Exercise: Add FlightlessBird Class

Exercise : Add FlightlessBird Class

Do the following changes to the given code so that the code below produces the given output:

  1. update the info() method of the Bird to call the same method from the parent class
  2. add a FlightlessBird class that inherits from the Bird class, and override the move() method
class Animal:

  def __init__(self, name):
    self.name = name
    
  def info(self):
    print("I'm a", self.name)
    

class Bird(Animal):

  def move(self):
    print("I'm flying")
    
  def info(self):
    # CALL info() METHOD OF PARENT CLASS HERE 
    print("I have feathers")

# ADD FlightlessBird CLASS HERE

crow = Bird('Crow')
crow.info()
crow.move()
penguin = FlightlessBird('Penguin')
penguin.info()
penguin.move()
 → 

I'm a Crow
I have feathers
I'm flying
I'm a Penguin
I have feathers
I'm waddling

Partial solution



Note that all python classes automatically inherits from the built-in class object even if you did not specify it as the parent class. The object class has a __str__() method that you can ovrride in your classes to customize how the print function will print an object of your class.

The Book class below overrides the __str__() method so that Book objects can be printed in a specific format.

class Book:
  def __init__(self, title):
    self.title = title

  def __str__(self):
    return 'Book title: ' + self.title

book = Book('Python for Beginners')
print(book)
 → 

Book title: Python for Beginners

Exercise: Override __str__() Method

Exercise : Override __str__() Method

Override the __str()__ of the Person class so that the code below produces the given output.

class Person:
  
  def __init__(self, name, age):
    self.name = name
    self.age = age
  
  # OVERRIDE __str__ METHOD HERE
    
print(Person('Amy', 25))
print(Person('Ben', 26))
 → 

Amy (25years)
Ben (26years)

Partial solution



A class can inherit from multiple classes. If multiple parent classes have the same method, the one that is given first (in the order of inheritance) will be used.

  

The TeachingAssistant class above inherits from both Student class and the Teacher class both of which inherit from the Person class. That means a TeachingAssistant object can use methods from classes object, Person, Student, Teacher, and TeachingAssistant.

elsie = TeachingAssistant('Elsie', 'A223344')
elsie.print_info()
elsie.teach()
elsie.learn()
elsie.grade()
 → 

Elsie is a teacher
Elsie is teaching
Elsie is learning
Elsie is grading

As both Teacher and Student classes have the print_info() method, the method from the Teacher class will be used as it comes first in the inheritance order (Teacher, Student); that is why you see Elsie is a teacher in the output instead of Elsie is a student.

Exercise: Add SmartPhone Class

Exercise : Add SmartPhone Class

Add a SmartPhone class to inherits from the Camera class and the Phone class so that the code below produces the given output.

class Camera:
  
  def take_photo(self):
    print('taking photo')
    
class Phone:
  
  def make_call(self):
    print('making call')
    
# ADD SmartPhone CLASS HERE

iphone = SmartPhone('iPhone')
iphone.take_photo()
iphone.make_call()
iphone.play_game()
 → 

My model: iPhone
taking photo
making call
playing game

Partial solution



You need to use inheritance when you create user-defined exceptions because all such exceptions need to inherit from a built-in Exception class.

In the example below, EmptyCommandError and InvalidCommandError are user-defined exceptions. The latter has overridden the constructor to take additional parameters.

class EmptyCommandError(Exception):
    """Indicates a task has expired."""
    pass


class InvalidCommandError(Exception):
    """Indicates that the user entered an invalid command"""

    def __init__(self, command, explanation):
        self.command = command
        self.explanation = explanation


def execute_command(command):
    if command == '':
        raise EmptyCommandError()
    elif len(command) < 4:
        raise InvalidCommandError(command, "command too short")


def process(command):
    try:
        execute_command(command)
    except EmptyCommandError:
        print('empty command')
    except InvalidCommandError as e:
        print('invalid command:', e.command, '->', e.explanation)


process('')
process('HA')

empty command
invalid command: HA -> command too short

Guidance for the item(s) below:

Let us learn how to automate the testing of Python code using unit tests (one of the testing types you learned under this week's SE topics).

Video

Appendix D: Unit Testing

The built-in module unittest supports automation of unit testing in an object-oriented way.

Let's assume you have a file called search.py which has the following two functions.

in search.py
def get_first_name(name):
  """Return the first part of the parameter 'name'"""
  return name.split()[0]


def is_same_person(person, keyword):
  """Return True if the parameter 'person' (type: dictionary)
  contains a key 'name' whose value contains the 
  parameter 'keyword' (type: string)
  e.g., 
  * is_same_person({'name': 'jackie'}, 'jack') returns True
  * is_same_person({'name': 'jackie'}, 'jackie-chan') returns False
  """
  return keyword in person['name']

This is how we can write some unit tests for the two functions.

in test_search.py
import search, unittest

class TestSearch(unittest.TestCase):

  def test_is_same_person(self):
    jack = {'name':'jack'}
    self.assertTrue(search.is_same_person(jack, 'jack'))
    self.assertTrue(search.is_same_person(jack, 'ack'))
    self.assertTrue(search.is_same_person(jack, 'ac'))
    self.assertTrue(search.is_same_person(jack, 'j'))
    self.assertTrue(search.is_same_person(jack, 'k'))
    self.assertFalse(search.is_same_person(jack, 'jackie'))
    self.assertFalse(search.is_same_person(jack, 'blackjack'))
    self.assertFalse(search.is_same_person({'name': 'x', 'other': 'jack'}, 'jack'))
    with self.assertRaises(KeyError):
      search.is_same_person({}, 'jack')
  
  def test_get_first_name(self):
    self.assertEqual(search.get_first_name('Amy'), 'Amy')
    self.assertEqual(search.get_first_name('Amy Bernice'), 'Amy')
    self.assertEqual(search.get_first_name('Amy-Bernice'), 'Amy-Bernice')
    with self.assertRaises(IndexError):
      search.get_first_name('')

# activate the test runner
if __name__ == '__main__':
    unittest.main()

When you run the above code, each method in the test class will be executed by a built-in test runner and the result will be reported. An example is given below:

...
----------------------------------------------------------------------
Ran 2 tests in 0.019s

OK

Things to note:

  • A class containing unit tests should inherit from unittest.TestCase
  • Names of functions containing test code should start with test e.g., test_is_same_person()
  • These methods (inherited from the parent class) can be used to compare actual with the expected value:
    • assertTrue(actual) : test passes if actual == True
    • assertFalse(actual) : test passes if actual == False
    • assertEquals(actual, expected) : test passes if actual == expected
    • with self.assertRaises(Exception): passes if the code block it contains raises the specified exception.

If the expected value is not as same as the actual, the test runner will report the test failure. For example, if we were to insert this statement into test_get_first_name method,

self.assertEqual(search.get_first_name('Amy Foo'), 'Foo')

the output will be something like this:

F.
======================================================================
FAIL: test_get_first_name (__main__.TestSearch)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main.py", line 19, in test_get_first_name
    self.assertEqual(search.get_first_name('Amy Foo'), 'Foo')
AssertionError: 'Amy' != 'Foo'
- Amy
+ Foo


----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

📎 Resources: