Object-oriented programming

This notebooks contains assingments that are more complex. They are aimed at students who already know about object- oriented Programming from prior experience and who are familiar with the concepts but now how OOP is done on Python.

The scope of this introduction is insufficient to give a good tutorial on OOP as a whole.

Our first example is an example of composition. We create a Company that consists a list of Persons and an account balance.

Then after we structure our classes with desired behaviour we can use them quite freely.

Create a class called Person for storing the following information about a person:

  • name

Create a method say_hi that returns the string "Hi, I'm " + the person's name.


In [ ]:
class Person:
    def __init__(self, name):
        pass
    
    def say_hi(self):
        pass

Run the following code to test that you have created the person correctly:


In [ ]:
persons = []
joe = Person("Joe")
jane = Person("Jane")
persons.append(joe)
persons.append(jane)

Now create a class Employee that inherits the class Person. In addition to a name, Employees have a title (string), salary (number) and an account_balance (number).

Override the say_hi method to say "Hi I'm " + name + " and i work as a " + title


In [ ]:
# the reference to Person on the line below means that the object inherits 
# Employee
class Employee(Person):
    def __init__(self, 
                 name, 
                 salary, 
                 title="Software Specialist", 
                 account_balance=0):
        # this calls the constructor of Person class
        super().__init__(name)
        # you still need to implement the rest
        pass
        
    def say_hi(self):
        pass

Every employee is also a person.


In [ ]:
persons = []
joe = Person("Joe")
jane = Person("Jane")
persons.append(joe)
persons.append(jane)
emp1 = Employee("Jack", 3000)
emp2 = Employee("Jill", 3000)
persons.append(emp1)
persons.append(emp2)
for person in persons:
    print(person.say_hi())

Now create a class called Company, which has a name and a list of Employee objects called employees and an account balance for the company.

Make a method payday(self) that will go through the list of employees and deduct their salary from the corporate account and add it to the employee account. Before you start deducting money compute the sum of salaries and make sure it is higher than the account balance. If it is not, raise an instance of the NotEnoughMoneyError.

Make a method layoff(self) that will remove one employe from the list of employees. If there are no more employees raise a NoMoreEmployeesException.


In [ ]:
class NotEnoughMoneyError(Exception):
    pass

class NoMoreEmployeesError(Exception):
    pass

class Company(object):
    def __init__(self, title, employees = [], account_balance=0):
        pass
        

    def payday(self):
       pass

    def layoff(self):
       pass

Okay, you've worked this far just creating the model, let's put it to use.

Make a method smart_payday(company). The method should attempt to call the payday method of the company. If the call raises a NotEnoughMoneyException lay off a worker and then try again. Don't catch the NoMoreEmployeesException as that should be handled at a higher level.

You will probably need to use a while loop to implement the re-trying until a condition is met.


In [ ]:
def smart_payday(company):
    pass

In [ ]:
# a bit of test code
names_and_salaries = [
    ("Jane", 3000), 
    ("Joe", 2000),
    ("Jill", 2000),
    ("Jack", 1500)
]

workers = [Employee(name, salary) \
           for name, salary in names_and_salaries]
scs = Company("SCS", employees=workers, account_balance=12000)

smart_payday(scs)
print(scs.account_balance)
print(len(scs.employees))
smart_payday(scs)
print(scs.account_balance)
print(len(scs.employees))
print(scs.employees)

Observe how printing the employees list is not very informative? Adding a magic method called __repr__ will help with that.

Extra: more Exceptions

Consider the following method, it will raise errors randomly. This type of failure is pretty common for IO-related tasks.


In [ ]:
class RandomException(Exception):
    pass
    

def do_wonky_stuff():
    import random
    if random.random() > 0.5:
        raise RandomException("this exception happened randomly")
    return

Wrap a call to do_wonky_stuff with a try-except clause.


In [ ]:
do_wonky_stuff()
print("yay it worked")

OK, now let's go even deeper.


In [ ]:
class ReallyRandomException(Exception):
    pass

def do_really_wonky_stuff():
    import random
    val = random.random()
    if val > 0.75:
        raise RandomException("this exception happened randomly")
    elif val < 0.15:
        raise ReallyRandomException("This exception is actually quite rare")
    return

Wrap do_really_wonky_stuff in a try-except -clause with two excepts. In the rarer of the excepts print out something so you'll if it's your lucky day.

In real life you'd probably want to handle different errors in a different way, or at least log or inform the user of what caused the error.


In [ ]:
do_really_wonky_stuff()