...

Sunday, May 26, 2013

Computer Science 09

Objection-Oriented Programming (OOP) is actually not a new notion. However, OOP didn't take off until Java, which was the first popular OOP language. The basis of OOP is the abstraction of data types. The idea is that we can extend our programming languages through user-defined types. These types can then be used just as though it's a built-in type.

The reason why it's called "Abstract" data type is because for each type there exists an Interface for which we can Implement. Interfaces explain what the methods do (but not how they do it). The Specification of an object tells us what the object does.

To define a Class, we use:

class class_name(object):
    #Contents of the Class


We create the Instantiator as such:

def __init__(self):
    #Contents of the instantiator


Every time the object is created, the __init__ method will be executed first. We usually introduce attributes of objects through its Instantiator like this:

def __init__(self):
    self.a=10
    self.L=[]


We then create the Object in the main code as such:

object_name = class_name()

Notice that there is a notion of self in the implementation. In __init__, self is a formal parameter, but when creating the object, we did not actually pass any parameter in.

This is because for every method in an object, the reference pointer of itself is automatically passed into the first parameter. Therefore, you'll need to include self (or anything you name) in every method's first formal parameter. The pointer in object_name and the self object inside __init__ is actually the same.

Integer a and list L are attributes of the instance of the class_name object, which is equivalent to the self object.

 To see the pointer of the object, we can print object_name directly, and this is what we'll see:

<__main__.class_name object at 0x0000000002C27B38>

This is also exactly what we'll see if we printed self inside the methods of the object.

Let's say that if we print object_name, we want to see a custom string representation of it, instead of its contents. We can do this by creating  the __str__ method:

def __str__(self):
    return "The value of a is: "+str(self.a)+"\nThe contents of L is "+str(self.L)


The print function automatically calls the __str__ method when attempting to print object_name.

If we want to let the value of a represent object_name when type-casted to an integer, we can simply create a method as such:

def __int__(self):
    return self.a


We can test by using:

print(10+int(object_name))

We can actually use class_name.a to refer to the "a" attribute in the object. However, this is not recommended. This is because "a" is a variable used in the implementation of the object. It is not in the specification that "a" even exists.

Take for instance, you have a method get_number() which returns the value of a certain number stored in variable "a". In a future version, let's say that the variable "a" doesn't exist anymore, because the implementation is different and uses perhaps variable "x" instead, to store that number.

If our programs used to reference "a", you would get a whole load of errors. However, if it used get_number(), it doesn't matter whether "a" or "x" is used, because it'll depend what's returned by the get_number() method. We still get the number we want.

The concept is known as Data Hiding, in which we abstract data into Getter and Setter methods. It is important that we do not directly access instance (created each instance of the class) and class variables (shared among all instances of the class), due to object reference problems and possibility of changes in the future.

Take for instance, we want to write a program to keep track of staff information across various departments of a company. To do this, we need to start thinking about the level of abstraction we want to have. Before we write the code in detail, we want to think about the types that would make it easier to write the code.

When dealing with stuff like these, in the end we don't want to deal with lists and dicts and floats. Instead, we want to keep track of things like department, worker, manager, etc.

We can then set up a system of hierarchy of these. We first identify the similarities between the objects. For example, worker and manager are all employees.  All employees have attributes like name, birth date, etc. We can set up the employees class as shown:

class Employee(object):
    import datetime
   
    def __init__(self,name):
        nameSplit=name.split()
        self.firstName=nameSplit[0]
        try:
            self.lastName=nameSplit[1]
        except:
            self.lastName=""
        self.birthdate=None

    def set_firstName(self,firstName):
        self.firstName=firstName

    def get_firstName(self):
        return self.firstName

    def set_lastName(self,lastName):
        self.lastName=lastName

    def get_lastName(self):
        return self.lastName

    def set_birthdate(self,date):
        self.birthdate=date

    def get_birthdate(self):
        return self.birthdate.isoformat()

    def get_age(self):
        return round((datetime.date.today()-self.birthdate).days/365.0,2)

    def __lt__(self,other):
        #If they have the same first names
        if self.firstName==other.firstName:
            #Compare their last names
            return self.lastName        else:
            #Else compare by their first names
            return self.firstName
 

    def __str__(self):
        return self.firstName+" "+self.lastName


Notice that there's a __lt__ method in there. The __lt__ method is what's called when you compare employee with something else. For example, if you have two employees, john and william, typing john < william would invoke __lt__(john,william).When sorting lists of employees, the __lt__ automatically gets used when comparing objects. Observe the following implementation in the main code:

listEmployees=[]
listEmployees.append(Employee("John Smith"))
listEmployees.append(Employee("William Smith"))
listEmployees.append(Employee("John Doe"))
listEmployees.append(Employee("William Wise"))

print("Before sort:")
for employeeInList in listEmployees:
    print(employeeInList)

print("After sort:")
listEmployees.sort()

for employeeInList in listEmployees:
    print(employeeInList)


It will be sorted according to first name, then last name, as implemented in the __lt__ method.

We can also play with the birthdate and age as shown:

def isOlder(employee1,employee2):
    if employee1.get_age()<employee2.get_age():
print(employee2,"is older")
    else:
          print(employee1,"is older")

import datetime

kelvin=Employee("Kelvin Ang")
kelvin.set_birthdate(datetime.date(1990,6,21))

ling=Employee("Lingling Pang")
ling.set_birthdate(datetime.date(1993,10,19))

print(kelvin,"is",kelvin.get_age())
print(ling,"is",ling.get_age())

isOlder(kelvin,ling)


As we can see, employee is a subclass of object, that's why we can do so much stuff with it. A worker is a subclass of employee. For example, in the company, every worker has a workerID. We can then do:

from Employee import Employee

class Worker(Employee):
    workerID=0
    def __init__(self,name):
        Employee.__init__(self,name)
        self.workerID=Worker.workerID
        Worker.workerID+=1

    def __str__(self):
        return Employee.__str__(self)+", Worker ID "+str(self.workerID)

    def __lt__(self,other):
        return self.workerID


We can then create a worker as such:

kelvin = Worker("Kelvin Ang")
print(kelvin)


(Note that I saved the Employee class in a file called Employee.py. I also didn't create the Getter and Setter methods for workerID, which should be implemented but had been left out in the interest of simplicity)

Notice that even though we inherited all methods from Employee class, we defined the __init__ method again. This is known as Method Overriding. When we Override a method, and we want its Superclass's __init__ to be run as well, we specify it in the Overriding Method.

Similarly, we Override the __str__ method to now include the Worker ID. We append the old output with our new information. If we didn't override __str__, it would use the superclass's __str__ method.

Notice that there's a workerID=0 statement at the top. This defines a Class Variable. A Class Variable is one that is shared among all objects of the same class. In this case, the first object instantiated gets assigned ID 0, and the next one gets assigned ID 1 and so on.

We can then try it out like this:

kelvin = Worker("Kelvin Ang")
ling = Worker("Lingling Pang")
print(kelvin)
print(ling)
print(kelvin<ling)


Note that we're now comparing the ID when we do kelvin<ling. Suppose that we want to compare names instead. We can use:

print(Employee.__lt__(kelvin,ling))

We, however, cannot use Worker.__lt__() for two employees, because it makes use of workerID which employees do not have.

We can also create another Class that is exactly the same as Worker. Like this:

class Worker2(Worker):
    pass


It may seem as though it's for nothing, but it allows us to do checking like this:

if type(kelvin)=='Worker'
    #Some code here
elif type(kelvin)=='Worker2'
    #Some other code here

No comments :

Post a Comment

<