Introduction to Object Oriented Programming

OOP

Motivation

The OOP is a technique which is useful in big projects (above ~500 lines). For smaller codes (like in our case) we deliberately pretend it to be complex to understand what is happening.

For greater projects it makes the writing and understanding of the code easier.

Look at the following piece of code which handles the courses of a student:

In [1]:
def hourstotal(mycourses, allcourses):
    hours = 0
    for course in allcourses:
        if course[0] in mycourses:
            hours += course[1]
    return hours

def credittotal(mycourses, allcourses):
    credits = 0
    for course in allcourses:
        if course[0] in mycourses:
            credits += course[2]
    return credits

# Format of one entry: (name, hours to attend, credit)
all_courses = [
    ("Info1", 3, 4),
    ("Info2", 3, 3),
    ("Combi1", 4, 4),
    ("Combi2", 3, 3)]

my_courses = ["Info1", "Combi1"]
print hourstotal(my_courses, all_courses)
print credittotal(my_courses, all_courses)
7
8

What happens if you want to modify a course entry. You want to store the presense requirements also.

In [2]:
def hourstotal(mycourses, allcourses):
    hours = 0
    for course in allcourses:
        if course[0] in mycourses:
            hours += course[1]
    return hours

def credittotal(mycourses, allcourses):
    credits = 0
    for course in allcourses:
        if course[0] in mycourses:
            credits += course[3] # <<< modified here
    return credits

# Format of one entry: (name, hours, presence required, credit)
all_courses = [
    ("Info1", 3, 2, 4),
    ("Info2", 3, 2, 3),
    ("Combi1", 4, 2, 4),
    ("Combi2", 3, 1, 3)]

my_courses = ["Info1", "Combi1"]
print hourstotal(my_courses, all_courses)
print credittotal(my_courses, all_courses)
7
8

Even if you store the same values except one, you have to modify the credittotal function, even if the credit entry did not change.

This is problematic if you have complicated data in the entries. And also you can forget to rewrite some of the functions when you insert new data.

This is why the tuple is not sustainable. One solution is to use a dictionary:

In [3]:
def hourstotal(mycourses, allcourses):
    hours = 0
    for course in allcourses:
        if course["name"] in mycourses:
            hours += course["hour"]
    return hours

def credittotal(mycourses, allcourses):
    credits = 0
    for course in allcourses:
        if course["name"] in mycourses:
            credits += course["credit"] # <<< you don't have to modify any more
    return credits

# Format of one entry: (name, classes in hours, presence required, credit)
all_courses = [
    {"name": "Info1", "hour": 3, "presence": 2, "credit": 4},
    {"name": "Info2", "hour": 3, "presence": 2, "credit": 3},
    {"name": "Combi1", "hour": 4, "presence": 2, "credit": 4},
    {"name": "Combi2", "hour": 3, "presence": 1, "credit": 3},]

my_courses = ["Info1", "Combi1"]
print hourstotal(my_courses, all_courses)
print credittotal(my_courses, all_courses)
7
8

This is not so bad, but there are still some problems.

  • If you add a new course you have to fill the dictionary carefuly.
    • you may forget to add a key, for example you have "name" but don't have "presence"
  • If you handle entries in more than one place, then you have to do the same in several places
    • this is problematic because you can do correctly in one place, but incorrectly in other places (code repetition).

You can solve this by adding a newcourse function which creates a correct course entry. In this case you have to use that function every time you create a new course entry.

In [4]:
def hourstotal(mycourses, allcourses):
    hours = 0
    for course in allcourses:
        if course["name"] in mycourses:
            hours += course["hour"]
    return hours

def credittotal(mycourses, allcourses):
    credits = 0
    for course in allcourses:
        if course["name"] in mycourses:
            credits += course["credit"]
    return credits

# you have to use this function when creating a new course
def newcourse(name, hour, precense, credit):
    return {"name" : name, "hour" : hour,
            "precense" : precense, "credit" : credit}

all_courses = [
    newcourse("Info1", 3, 2, 4),
    newcourse("Info2", 3, 2, 3),
    newcourse("Combi1", 4, 2, 4),
    newcourse("Combi2", 3, 1, 3)]

my_courses = ["Info1", "Combi1"]
print hourstotal(my_courses, all_courses)
print credittotal(my_courses, all_courses)
7
8

This solves the problem of incorrect or incomplete entries if you use the newcourse function. Since you cannot call that function without the proper parameters.

This solution is the closest to the concept of a class.

Class and object

The class is similar to a type, like list and an object is an instance of a class.

Like 5 is an instance of int.

Let's make a class named Course. It is a custom to use capitalized names as class names. However python itself does not follow this rule (like list).

In [5]:
class Course(object):
    pass

This works, but does nothing. One can create an instance by the name of the class and a parenthesis:

In [6]:
c = Course()

And you can add values to that instance:

In [7]:
c.name = "Info2"
c.hour = 3

print c.hour
3

You can access the members with the dot (.) operator. The instance is on the left-hand-side of the dot and the required member is on the right-hand-side.

The member can be a value and a function, like the append of a list.

Constructor

You can add data members after creating the instance, it is better to add them during the creation.

This is the constructor:

In [8]:
class Course(object):
    def __init__(self, name, hour, precense, credit):
        self.name = name
        self.hour = hour
        self.precense = precense
        self.credit = credit

The constructor is a special method called __init__.

That function is executed when an instance is created: write the name of the class and after that the parameters in a paranthesis.

c = Course( x, y, ... )

The self parameter is the object to be created, you can set the members using the function parameters.

Let's see the course example with a class:

In [9]:
def hourstotal(mycourses, allcourses):
    hours = 0
    for course in allcourses:
        if course.name in mycourses:
            hours += course.hour
    return hours

def credittotal(mycourses, allcourses):
    credits = 0
    for course in allcourses:
        if course.name in mycourses:
            credits += course.credit
    return credits

all_courses = [
    Course("Info1", 3, 2, 4),
    Course("Info2", 3, 2, 3),
    Course("Combi1", 4, 2, 4),
    Course("Combi2", 3, 1, 3)]

my_courses = ["Info1", "Combi1"]
print hourstotal(my_courses, all_courses)
print credittotal(my_courses, all_courses)
7
8

Now let's make a class representing complex numbers, called Complex.

In [10]:
class Complex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary

z = Complex(4, 3)
print z.re, z.im
4 3

We write an add function:

In [11]:
class Complex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
def complex_add(z1, z2):
    new_re = z1.re + z2.re
    new_im = z1.im + z2.im
    return Complex(new_re, new_im)

z1 = Complex(4, 3)
z2 = Complex(-2, 1)
z3 = complex_add(z1, z2)

print z3.re, z3.im
2 4

Method

It is better to put the add function inside the class definition, since it is used to add Complex numbers and nothing else.

In [12]:
class Complex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
    def add(z1, z2):
        new_re = z1.re + z2.re
        new_im = z1.im + z2.im
        return Complex(new_re, new_im)

z1 = Complex(4, 3)
z2 = Complex(-2, 1)
z3 = Complex.add(z1, z2)

print z3.re, z3.im
2 4

Even better without writing the name of the class in front of the function. The following is a method:

In [13]:
class Complex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
    def add(self, z2):
        new_re = self.re + z2.re
        new_im = self.im + z2.im
        return Complex(new_re, new_im)

z1 = Complex(4, 3)
z2 = Complex(-2, 1)
z3 = z1.add(z2) # <<< method

print z3.re, z3.im
2 4

The method's first parameter is self, which is the object on the left-hand-side of the dot. The method's name is on the right-hand-side of the dot. After that the other parameters.

In this case we say: add z2 to self (which is now z1).

Special methods

It can be even nicer.

In [14]:
class Complex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
    def __add__(self, z2):
        new_re = self.re + z2.re
        new_im = self.im + z2.im
        return Complex(new_re, new_im)

z1 = Complex(4, 3)
z2 = Complex(-2, 1)
z3 = z1 + z2 # <<< the __add__ method is called

print z3.re, z3.im
2 4

The special name __add__ marks an operator, namely + but you can call it by the exact name:

In [15]:
z4 = z1.__add__(z2)
print z4.re, z4.im
2 4

The special method __add__ is called when there is a + operator after a Complex instance.

The left-hand-side of the + becomes self and the right-hand- side is the parameter. The result will be the returned object.

A special method can be a number of things, like __sub__, __mul__, __div__. But the __init__ was that, too.

These are special because they can be called other than their name. These special names are defined by python. They always start with a double underscore and end with a double underscore.

Printing

There is a small problem left:

In [16]:
print z3
<__main__.Complex object at 0x0000000004F4BCC0>

This is not nice, but there is a special method used for printing:

In [17]:
class Complex(object):
    def __init__(self, real, imaginary):
        self.re = real
        self.im = imaginary
        
    def __add__(self, z2):
        new_re = self.re + z2.re
        new_im = self.im + z2.im
        return Complex(new_re, new_im)
    
    # used by print
    def __str__(self):
        return str(self.re) + " + " + str(self.im) + "i"

z1 = Complex(4, 3)
z2 = Complex(-2, 1)
z3 = z1 + z2

print z1
print z2
print z3
4 + 3i
-2 + 1i
2 + 4i
In [18]:
str(Complex(3, 2))
Out[18]:
'3 + 2i'

The __str__ method should return a string, that is what will be printed. This method is called when the user wants to print the object or convert it to string.

However that is not perfect yet:

In [19]:
print Complex(0, 0)    # 0
print Complex(3, 0)    # 3
print Complex(-2, 1)   # -2 + i
print Complex(-2, -1)  # -2 - i
0 + 0i
3 + 0i
-2 + 1i
-2 + -1i
In [ ]: