7  Object Oriented Programming

Object Oriented Programming (OOP) or Object Oriented Design (OOD) is a programming paradigm or way of working when developing software. The main idea is to structure code according to common properties and features of (real-world) objects.

In Python this is achieved by Classes as the abstract definition and Objects as a concrete realisation.

7.1 Object Oriented Programming in Python

The best way to explain OOP is by defining a class and objects. So let us start creating a class.

Tip

Right from the start we will include doc strings as descriptions and we use type hints for the functions to give an orientation.

7.2 Definition of attributes

1class Robot:
    """ Class representing a robot. """

    def __init__(self, 
                 name: str,
                 ip: list[int],
                 port: int,
2                 speed: float) -> None:
        """ Initialize the name and the ip address of the robot"""
3        self.name = name
        self.ip = ip
4        self.__port = port
        self.speed = speed
1
The common convention is that classes use a capital letter.
2
The constructor of the class is defined with __init__ and self indicated that the method belongs to an object.
3
We use attributes to store certain aspects of a object where we use self.name to identify the scope.
4
We can hide attributes such that they can not be accessed from outside.

Now that we have a simple class we can define our first objects.

r2d2 = Robot("R2D2", [0, 0, 0, 1], 443, 32)
number5 = Robot("Number 5", [0, 0, 0, 3], 80, 20)

print(f"I am called {r2d2.name}, I can be reached under "
      f"{'.'.join(str(s) for s in r2d2.ip)}"
      f" and my top speed is {r2d2.speed}!")
print(f"I am called {number5.name}, I can be reached under "
      f"{'.'.join(str(s) for s in number5.ip)}"
      f" and my top speed is {number5.speed}!")
I am called R2D2, I can be reached under 0.0.0.1 and my top speed is 32!
I am called Number 5, I can be reached under 0.0.0.3 and my top speed is 20!

Hidden properties can not be access from outside:

print(f"{r2d2.__port=}")
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 1
----> 1 print(f"{r2d2.__port=}")

AttributeError: 'Robot' object has no attribute '__port'

The little introduction each of the robots provides us with should be part of the class, not a property but a method.

7.2.1 Definition of methods

class Robot:
    """ Class representing a robot. """

    def __init__(self, 
                 name: str,
                 ip: list[int],
                 port: int,
                 speed: float) -> None:
        """ Initialize the name and the ip address of the robot"""
        self.name = name        
        self.ip = ip            
        self.__port = port      
        self.speed = speed      
                                

    def introduction(self) -> str:
        """ Short introduction of the robot"""
        return (f"I am called {self.name}, I can be reached under "
4                f"{self.__ip2str()} and my top speed is {self.speed}!")
    

    def __ip2str(self) -> str:
        """ Transform the integer set with the port into a string"""
1        return f"{'.'.join(str(s) for s in self.ip)}:{self.__port}"

2r2d2 = Robot("R2D2", [0, 0, 0, 1], 443, 32)
number5 = Robot("Number 5", [0, 0, 0, 3], 80, 20)

3print(r2d2.introduction())
print(number5.introduction())
1
We can use the hidden attribute.
2
We need to redefine the objects for the new class.
3
Calling a method is like calling a function but for the class object.
4
We can call other methods inside our object and they can be hidden.
I am called R2D2, I can be reached under 0.0.0.1:443 and my top speed is 32!
I am called Number 5, I can be reached under 0.0.0.3:80 and my top speed is 20!
Important

To summarize our first findings.

  1. Class:
    1. Our class Robot is an abstract description of all the Robots we can think about.
    2. We can have properties that are described by attributes and actions described by methods.
    3. We can define hidden attributes and methods if they should only be access from within the object.
  2. Object:
    1. A specific object like r2d2 is an instance of the class Robot and keeps track of its own set of attributes.
    2. The methods are shared with all and we can specify and use our attributes.

We can see a lot of similarities between classes and modules, how we call specific functions attributed to modules and how they are organised within Python.

7.2.2 Overwriting methods and integration with operators

A lot of the base types we have been using are actually classes and we where dealing with the objects. It stands to reason, that we should be able to treat them similar.

print(f"{r2d2==number5=}")
print(f"{r2d2<number5=}")
print(r2d2)
print(r2d2 + number5)
r2d2==number5=False
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 2
      1 print(f"{r2d2==number5=}")
----> 2 print(f"{r2d2<number5=}")
      3 print(r2d2)
      4 print(r2d2 + number5)

TypeError: '<' not supported between instances of 'Robot' and 'Robot'

In order to allow a seamless integration of our object into these types we should implement a couple of the basic customizations

class Robot:
    """ Class representing a robot. """

    def __init__(self, 
                 name: str,
                 ip: list[int],
                 port: int,
                 speed: float) -> None:
        """ Initialize the name and the ip address of the robot"""
        self.name = name        
        self.ip = ip            
        self.__port = port      
        self.speed = speed      

    def introduction(self) -> str:
        """ Short introduction of the robot"""
        return (f"I am called {self.name}, I can be reached under "
                f"{self.__ip2str()} and my top speed is {self.speed}!")
    
    def __ip2str(self) -> str:
        """ Transform the integer set with the port into a string"""
        return f"{'.'.join(str(s) for s in self.ip)}:{self.__port}"     

    def __ep__(self, other):
        """ Compare two objects of type Robot"""
        return self.speed == other.speed

    def __lt__(self, other):
        """ Less than for Robot"""
        return self.speed < other.speed

    def __str__(self):
        """ The official string representation """
        return self.introduction()
    
    def __add__(self, other):
        return Robot(self.name + "♥" + other.name, 
                     list(map(lambda x, y: x + y, self.ip, other.ip)),
                     self.__port,
                     min(self.speed, other.speed))


r2d2 = Robot("R2D2", [0, 0, 0, 1], 443, 32)                         
r2d3 = Robot("R2D3", [0, 0, 0, 2], 443, 32)                         
number5 = Robot("Number 5", [0, 0, 0, 3], 80, 20)

print(f"{r2d2==number5=}")
print(f"{r2d2==r2d3=}")
print(f"{number5<r2d2=}")
print(f"{r2d2<number5=}")
print(r2d2)
print(r2d2 + number5)
r2d2==number5=False
r2d2==r2d3=False
number5<r2d2=True
r2d2<number5=False
I am called R2D2, I can be reached under 0.0.0.1:443 and my top speed is 32!
I am called R2D2♥Number 5, I can be reached under 0.0.0.4:443 and my top speed is 20!

7.2.3 Inheritance

Quite often we want to define a class not from scratch but start form another class. This concept is called inheritance or we can formulate it as one class is the child of another class.

1class Cat:

    def __init__(self, name: str, habitat: list[str]) -> None:
        self.name = name
        self.habitat = habitat
        print(f"I live in {', '.join(habitat)}")

    def __str__(self) -> str:
        return f"My name is {self.name}, I am a Cat"

2class Tiger(Cat):

3    def __init__(self, name: str, habitat: list[str]=["Asia"]) -> None:
        super().__init__(name, habitat)

    def __str__(self) -> str:                               
4        return f"{super().__str__()} -> Tiger"

class Lion(Cat):
    
    def __init__(self, name: str, habitat: list[str]=["Africa", "India"]) -> None:
        super().__init__(name, habitat)

    def __str__(self) -> str:
        return f"{super().__str__()} -> Lion"

5class Liger(Lion, Tiger):

    def __str__(self) -> str:
        return (f"{super().__str__()} -> Liger"             
6               f" (Father {self.__class__.__bases__[0].__name__},"
               f" Mother {self.__class__.__bases__[1].__name__})")

7class Tigon(Tiger, Lion):

    def __str__(self) -> str:
        return (f"{super().__str__()} -> Tigon"
               f" (Father {self.__class__.__bases__[0].__name__},"
               f" Mother {self.__class__.__bases__[1].__name__})")

sven = Liger("Sven")
olson = Tigon("Olson")
print(sven)
print(olson)
Liger.__mro__
1
We can define a base class with a constructor and a way of producing a string.
2
We inherit from a class simply by putting the class name into the definition as argument.
3
If we inherit from the base we do not need to overwrite everything, only the methods we want to and we can be more specific like defining defaults.
4
We can access the parent with super(), similar to self.
5
We can inherit from multiple classes.
6
There are multiple ways of accessing properties of parent classes.
7
The order of the classes for inheritance matters.
I live in Africa, India
I live in Asia
My name is Sven, I am a Cat -> Tiger -> Lion -> Liger (Father Lion, Mother Tiger)
My name is Olson, I am a Cat -> Lion -> Tiger -> Tigon (Father Tiger, Mother Lion)
(__main__.Liger, __main__.Lion, __main__.Tiger, __main__.Cat, object)

7.3 Unified Programming Language

Now that we know about the basics of OOP and how we can deal with it in Python we can take a step back and look at an abstract interface that helps us work in the framework of OOP. The Unified Modelling Language (UML) can be used to define how classes look like and how they interact. It provides a language independent way of designing a OO program.

So let us extend our class Cat first with an UML diagram before we extend the code. In UML we see how classes interact but in general we will not see any specific object.

UML Class diagram

UML Class diagram

Furthermore, we can model different relationships with different arrow styles, how does this look like for our big cats:

classDiagram
    note "About big cats"
    Cat <|-- Lion
    Cat <|-- Tiger
    note for Tigon "Hybrid between male lion\nand female tiger"
    Tiger <|-- Liger
    Lion <|-- Liger
    Lion <|-- Tigon
    Tiger <|-- Tigon
    Cat : +String name
    Cat : +List~String~ habitat
    Cat : +String gender
    Cat : +int age
    Cat : -String mood
    Cat : -Cat father
    Cat : -Cat mother
    Cat : +breath()
    Cat : +mate(other Cat)
    Cat : +eat(food)
    Cat : +run(destination)
    Cat : +sleep(hours float)
    Cat : +meow()
    class Liger{
        -Lion father
        -Tiger mother
    }
    class Tigon{
        -Tiger father
        -Lion mother
    }
    class Lion{
        -Lion father
        -Lion mother
    }
    class Tiger{
        -Tiger father
        -Tiger mother
    }

Relationship between different classes

7.3.1 One way relationships

Besides inheritance we can have other relationships between classes.

classDiagram
Tiger --|> Cat : Inheritance
Mammal ..|> Animal : Generalization
Math -- Informatics : Association/Link
Professor --> Student : One way Association

Relationships in terms of arrows

  1. Inheritance - Tiger inherits from Cat - is a relation
  2. Generalization - Mammal implements/is a specific variant of Animal
  3. Association/Link - Math and Informatics call each other
  4. One way Association - Professor can call Student’s properties and methods but not visa versa

Of course we can also reflect other more complex relationships. An easy to understand example is the inclusion of a professor into a higher educational institution.

classDiagram
Department --* University : Composition
Professor --o Department : Aggregation
Professor ..> Salary : Dependency
Professor ..|> Academic : Realization

Relationships in terms of arrows part 2

  1. Composition - University has an instance of Department, Department cannot exist without University
  2. Aggregation - F has a instance of E. E can exist if F is not present
  3. Dependency - Professor requires, needs or depends on Salary
  4. Realization - Professor realizes the behaviour of Academic

7.3.2 Multiplicity

Quite often it is necessary to describe the relation in terms of multiplicity, i.e. specifying how often a class is used in the relationship.

Different multiplicities and their meaning
multiplicity meaning
1 exactly one
m exactly m
+ many, none or multiple, optional
0..1 none to one, optional
m..n m to n, including the boundary
1..* one or more
m..* m or more than m

Let us try to illustrate this with the example of a car on a parking lot:

classDiagram
Car "0..5" -- "1" Person : uses
ParkingLot "0..1" o-- "0..*" Car : occupied
Car "1" *.. "4" Wheel : has

Cardinality with cars on parking lots

You read this away from the class:

  • exactly four wheels belong to one car
  • a parking lot contains zero to infinity cars (not at the same time)
  • one car is standing on at most one parking lot
  • a person can only sit in one car
  • a car does not need to be occupied by a person but no more than 5

7.4 Principles of OOP

There are multiple principals of OOD and as the list on the german wikipedia is organised nicer we refer this version here.

We will pick a couple to give a first introduction.

7.4.1 Principle of abstraction

An abstraction only needs to be as accurate to the real world as the application requires. For example if you model an aeroplane for the dynamic simulation of the behaviour during flight you need a different model than for a ticket booking system.

classDiagram
    class Aeroplane{
        - speed
        - altitude
        - rollAngle
        - pitchAngle
        - yawAngle
        + fly()
    }

Aeroplane for a simulator

classDiagram
    class Aeroplane{
        - seats
        + reserveSeat(n)
    }

Aeroplane for a booking system

This is often referred to as the single responsibility principle.

7.4.2 Principle of encapsulation

We also want to make sure to encapsulate our objects as best as possible. So only allow access to the methods that are really needed from outside and not to everything, especially to properties, e.g. the aeroplane pitch angle should not be controlled by the airport.

In a similar way we can define interfaces. They are classes with abstract methods that can be implemented by another class.

In Python this is done with the Abstract Base Classes or ABC.

classDiagram
    Airport --> FlyingTransport : Dependency
    Helicopter ..|> FlyingTransport
    Aeroplane ..|> FlyingTransport
    Domesticated Gryphon ..|> FlyingTransport
    class FlyingTransport{
        <<interface>>
        + fly(origin, destination, passengers)
    }
    note for FlyingTransport "An interface in UML has only methods"
    class Helicopter{
        - ...
        + fly(origin, destination, passengers)
    }
    class Aeroplane{
        - ...
        + fly(origin, destination, passengers)
    }
    class Domesticated Gryphon{
        - ...
        + fly(origin, destination, passengers)
    }
    class Airport{
        - ...
        + accept(FlyingTransport vehicle)
    }

Airport and its flying inhabitants

from abc import ABC, abstractmethod

# Abstract Class
class FlyingTransport(ABC):
    @abstractmethod
    def fly(self, origin, destination, passengers):
        pass # Abstract Method has no implementation!

# Non-Abstract Class
class Helicopter(FlyingTransport):
    def fly(self, origin, destination, passengers):
        print("Helicopter flying")

# Non-Abstract Class
class Aeroplane(FlyingTransport):
    def fly(self, origin, destination, passengers):
        print("Aeroplane flying")

helicopter1 = Helicopter()
aeroplane1 = Aeroplane()

helicopter1.fly("INN", "QOJ", 2) #> Helicopter flying
aeroplane1.fly("INN", "BER", 200) #> Aeroplane flying
Helicopter flying
Aeroplane flying

7.4.3 Principle of inheritance

You can base a class on another, where a sub class will have (at least) the same interface as the super class. This allows you to create less copy-past code.

It is necessary that we always implement all the abstract methods we might inherit from interfaces.

7.4.4 Principle of polymorphism

Liskov substitution principle says that you can exchange an object within a program with a sub class of that object without breaking the program. Meaning, a sub class always needs to behave as the super class if you look at it like it would be a super class.

You can state this in nice mathematical formulas \[ S \leq T \to (\forall x: T.\phi(x) \to \forall y: S.\phi(y)), \] where \(T.\phi(x)\) is a property provable about object \(x\) of type T.

With regards to Python you can easily overwrite methods with the same name in different classes.

With this we close our quick excursion into Object Oriented Programming and move to Scientific Computing.