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 }
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.
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,
str,
name: list[int],
ip: int,
port: 2float) -> None:
speed: """ Initialize the name and the ip address of the robot"""
3self.name = name
self.ip = ip
4self.__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__
andself
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.
= Robot("R2D2", [0, 0, 0, 1], 443, 32)
r2d2 = Robot("Number 5", [0, 0, 0, 3], 80, 20)
number5
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,
str,
name: list[int],
ip: int,
port: float) -> None:
speed: """ 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 "
4f"{self.__ip2str()} and my top speed is {self.speed}!")
def __ip2str(self) -> str:
""" Transform the integer set with the port into a string"""
1return f"{'.'.join(str(s) for s in self.ip)}:{self.__port}"
2= Robot("R2D2", [0, 0, 0, 1], 443, 32)
r2d2 = Robot("Number 5", [0, 0, 0, 3], 80, 20)
number5
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!
To summarize our first findings.
- Class:
- Our class
Robot
is an abstract description of all the Robots we can think about. - We can have properties that are described by attributes and actions described by methods.
- We can define hidden attributes and methods if they should only be access from within the object.
- Our class
- Object:
- A specific object like
r2d2
is an instance of the classRobot
and keeps track of its own set of attributes. - The methods are shared with all and we can specify and use our attributes.
- A specific object like
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,
str,
name: list[int],
ip: int,
port: float) -> None:
speed: """ 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))
= Robot("R2D2", [0, 0, 0, 1], 443, 32)
r2d2 = Robot("R2D3", [0, 0, 0, 2], 443, 32)
r2d3 = Robot("Number 5", [0, 0, 0, 3], 80, 20)
number5
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):
3def __init__(self, name: str, habitat: list[str]=["Asia"]) -> None:
super().__init__(name, habitat)
def __str__(self) -> str:
4return 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"
6f" (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__})")
= Liger("Sven")
sven = Tigon("Olson")
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 toself
. - 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.
Furthermore, we can model different relationships with different arrow styles, how does this look like for our big cats:
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
- Inheritance -
Tiger
inherits fromCat
- is a relation - Generalization -
Mammal
implements/is a specific variant ofAnimal
- Association/Link -
Math
andInformatics
call each other - One way Association -
Professor
can callStudent
’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
- Composition -
University
has an instance ofDepartment
,Department
cannot exist withoutUniversity
- Aggregation - F has a instance of E. E can exist if F is not present
- Dependency -
Professor
requires, needs or depends onSalary
- Realization -
Professor
realizes the behaviour ofAcademic
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.
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
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() }
classDiagram class Aeroplane{ - seats + reserveSeat(n) }
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) }
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")
= Helicopter()
helicopter1 = Aeroplane()
aeroplane1
"INN", "QOJ", 2) #> Helicopter flying
helicopter1.fly("INN", "BER", 200) #> Aeroplane flying aeroplane1.fly(
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.