9. Object Oriented Programming

9.1. Introduction

A Programming Paradigm is a way or style of programming. There are many different paradigms and sometimes they overlap. Some of the main ones are:

The Imperative Paradigm can best be understood as a sequence of commands that are executed one by one as in: first do this, then do that, etc. It can contain loop and branch statements. The programs we have written so far follow this paradigm.

digraph {
"Programming Paradigms" -> "Imperative"
"Programming Paradigms" -> "Functional"
"Programming Paradigms" -> "Object Oriented"
"Programming Paradigms" -> "...others"
}

The Functional Paradigm uses mathematical functions as a core principle of building a program. Functions can then be called to evaluate an expression and use the resulting value for further calculations using functions. Functions can also be nested, i.e., a function that calls another function etc. We have used functions earlier, so imagine an almost exclusive use of functions throughout your program.

The Object Oriented Programming (OOP) Paradigm defines objects (i.e., typically a data construct that contains functions that can be called to execute calculations on its data) that send messages to each other.

Objects are like lists or arrays combined with functions that can be called on these lists or arrays. In essence an object is a container holding the data together with methods (or functions) that can be applied to these data. In OOP functions are typically referred to as methods.

import math as m
import time # Imports system time module to time your script

Before using an object, we need to define the blueprint for it. We use the class method to do this and define the blueprint for an animal-object.

Note

Later we will define more blueprints for sub-classes within animals that inherit some common features of animals and then add more specific features that are particular to that animal.

When defining object we make the rough distinction between data that the object carries - these are usually variables with the prefix .self and functions that can be called on these data and that are defined within the object. These functions are called methods in object-oriented parlance. In python they are defined very similarly to functions from the previous chapter.

import math as m

class Animal(object):
    """This is the animal class. It's the top class."""

    def __init__(self, name = 'Animal Doe', weight = 0.0):
        """Initialize the animal-object with a
        default name variable"""
        self.name = name
        self.weight = weight

    def showMyself(self):
        """Method: showMyself()
        prints out all the variables."""
        print("\n")
        print("Hello, let me introduce myself.")
        print("-------------------------------")
        print("My name is {}".format(self.name))
        print("My weight is {} pounds".format(self.weight))
        print("-------------------------------\n")

    def eatFood(self, foodAmount = 0.0):
        """Method: eatFood(foodAmount=0)
        translates food input 'foodAmount'
        into additional weight according to the
        following formula:
        new_weight = old_weight + sqrt(foodAmount)"""
        self.weight += m.sqrt(foodAmount)

You can look at the class blueprint of the animal object using the help() function

help(Animal)
Help on class Animal in module builtins:

class Animal(object)
 |  This is the animal class. It's the top class.
 |
 |  Methods defined here:
 |
 |  __init__(self, name='Animal Doe', weight=0.0)
 |      Initialize the animal-object with a
 |      default name variable
 |
 |  eatFood(self, foodAmount=0.0)
 |      Method: eatFood(foodAmount=0)
 |      translates food input 'foodAmount'
 |      into additional weight according to the
 |      following formula:
 |      new_weight = old_weight + sqrt(foodAmount)
 |
 |  showMyself(self)
 |      Method: showMyself()
 |      prints out all the variables.
 |
 |
----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables (if defined)
 |
 |  __weakref__
 |      list of weak references to the object (if defined)

We next generate an animal object. Here we create the first animal. Its name is Birdy and Birdy weighs 5 gramms.

animal1 = Animal(name = 'Birdy', weight = 5.0)

You can access the variables (or embedded data) of the animal using the animal object and the ‘dot’ notation.

print("The name of the first animal is {}".format(animal1.name))
print("The weight of the first animal is {}".format(animal1.weight))
The name of the first animal is Birdy
The weight of the first animal is 5.0

Or you can simply call the showMyself() method that was defined within the class or blueprint of the animal object.

animal1.showMyself()
Hello, let me introduce myself.
-------------------------------
My name is Birdy
My weight is 5.0 pounds
-------------------------------

Now we let the animal eat, by calling the method eatFood() on the bird object.

animal1.eatFood(9)
print("The animal's new weight is = {}".format(animal1.weight))
The animal's new weight is = 8.0

Another way to see the change is to call the same showMyself() method that we called earlier so you see that the animal1 object is actually storing the change in weight permanently.

animal1.showMyself()
Hello, let me introduce myself.
-------------------------------
My name is Birdy
My weight is 8.0 pounds
-------------------------------

9.2. Inheritance and Subclasses

We next generate a new class that inherits features of the animal class but is more specific. We call it the bird class. A bird is an animal but it has certain bird specific features that other animals do not share. Here is the blueprint.

Note that it is derived from the Animal class so that the Bird class inherits all the methods and variable definitions of its superclass of Animals. However, it adds a couple more variables and methods. Plus it overrides the old method showMyself() with a more comprehensive output.

Note

Inheritance saves us a lot of typing. As you generate other particual animal objects, you will not have to retype the methods that all animals share. This makes codes more condensed, re-usable and readable.

class Bird(Animal):
    """This is the bird class, derived from the
    animal class."""

    def __init__(self, name='Bird Doe', weight=0, color='na',
speed=0):
        """Initialize the bird-object calling the animal init method
        but also adding additional variables"""
        Animal.__init__(self, name, weight)
        self.color = color
        self.speed = speed

    def showMyself(self):
        """Method: showMyself()
        prints out all the variables."""
        print("\n")
        print("Hello, let me introduce myself.")
        print("-------------------------------")
        print("My name is {}".format(self.name))
        print("My weight is {} grams".format(self.weight))
        print("My color is {}".format(self.color))
        print("My speed is {}".format(self.speed))
        print("-------------------------------\n")

    def flyTraining(self, workoutLength=0):
        """Method: flyTraining(workoutLength)
        Augments the flight speed of the bird as a function
        of the bird objects workoutLength:
        new_speed = old_speed + log(workoutLength)"""
        self.speed += m.log(workoutLength)

Here we redefined the showMyself() method from the animal class. When we call it on the bird object, it will use this new definition of the method.

Now let’s look at the bird’s class blueprint

help(Bird)
Help on class Bird in module builtins:

class Bird(Animal)
 |  This is the bird class, derived from the
 |  animal class.
 |
 |  Method resolution order:
 |      Bird
 |      Animal
 |      object
 |
 |  Methods defined here:
 |
 |  __init__(self, name='Bird Doe', weight=0, color='na', speed=0)
 |      Initialize the bird-object calling the animal init method
 |      but also adding additional variables
 |
 |  flyTraining(self, workoutLength=0)
 |      Method: flyTraining(workoutLength)
 |      Augments the flight speed of the bird as a function
 |      of the bird objects workoutLength:
 |      new_speed = old_speed + log(workoutLength)
 |
 |  showMyself(self)
 |      Method: showMyself()
 |      prints out all the variables.
 |
 |
----------------------------------------------------------------------
 |  Methods inherited from Animal:
 |
 |  eatFood(self, foodAmount=0.0)
 |      Method: eatFood(foodAmount=0)
 |      translates food input 'foodAmount'
 |      into additional weight according to the
 |      following formula:
 |      new_weight = old_weight + sqrt(foodAmount)
 |
 |
----------------------------------------------------------------------
 |  Data descriptors inherited from Animal:
 |
 |  __dict__
 |      dictionary for instance variables (if defined)
 |
 |  __weakref__
 |      list of weak references to the object (if defined)

We next generate our first bird object.

bird1 = Bird('Herman', 12, 'blue', 40)
bird1.showMyself()
Hello, let me introduce myself.
-------------------------------
My name is Herman
My weight is 12 grams
My color is blue
My speed is 40
-------------------------------

We let the bird eat and then we train him a bit.

bird1.eatFood(foodAmount=9)
bird1.flyTraining(workoutLength=10)
bird1.showMyself()
Hello, let me introduce myself.
-------------------------------
My name is Herman
My weight is 15.0 grams
My color is blue
My speed is 42.30258509299404
-------------------------------

We next generate another bird, but this time we leave out some of the features when we generate it. Python will then simply assign the default values that we defined in the class blueprint above.

bird2 = Bird('Tweets', speed=12)
bird2.showMyself()
Hello, let me introduce myself.
-------------------------------
My name is Tweets
My weight is 0 grams
My color is na
My speed is 12
-------------------------------

9.3. Generating Multiple Objects

Generating a list full of bird objects from two lists with names and weights of animals, but no colors or speeds

birdObjectList = []
name_list = ['Birdy', 'Chip', 'Tweets', 'Feather', 'Gull']
weight_list = [4.3, 2.3, 5.6, 5.0, 15.3]

for i, (name, weight) in enumerate(zip(name_list, weight_list)):
    print("Nr. {}: name = {}, weight = {}".format(i, name, weight))
    # Here we create the bird objects and store them in
    # the birdObjectList
    birdObjectList.append(Bird(name=name, weight=weight))

# Here we print what we have so far
print(birdObjectList)
Nr. 0: name = Birdy, weight = 4.3
Nr. 1: name = Chip, weight = 2.3
Nr. 2: name = Tweets, weight = 5.6
Nr. 3: name = Feather, weight = 5.0
Nr. 4: name = Gull, weight = 15.3
[<Bird object at 0x7f9415e121d0>, <Bird object at 0x7f9415e12208>,
<Bird object at 0x7f9415e12240>, <Bird object at 0x7f9415e12278>,
<Bird object at 0x7f9415e122b0>]

We can access the bird objects created in this list by list indexation and calling on methods defined on the bird class.

Let’s graph the 3rd bird in the list and remember that indexing starts at 0.

print('birdObjectList[2] =', birdObjectList[2])
print('birdObjectList[2].name =', birdObjectList[2].name)
print('birdObjectList[2].weight =', birdObjectList[2].weight)
print('birdObjectList[2].color =', birdObjectList[2].color)
print('birdObjectList[2].speed =', birdObjectList[2].speed)
birdObjectList[2] = <Bird object at 0x7f9415e12240>
birdObjectList[2].name = Tweets
birdObjectList[2].weight = 5.6
birdObjectList[2].color = na
birdObjectList[2].speed = 0

Or we could have also done:

birdObjectList[2].showMyself()
Hello, let me introduce myself.
-------------------------------
My name is Tweets
My weight is 5.6 grams
My color is na
My speed is 0
-------------------------------