Objektorientierte Programmierung
Das objektorientierte Programmierparadigma ist der „State of the Art“ Ansatz um die Komplexität in Softwaresystemen beherrschbar zu machen. In den Grundlagen dieses Skriptums wurde die Funktion als Möglichkeit der Modularisierung von Quellcode eingeführt.
Die Verwendung von Funktionen impliziert, dass die Daten auf welche Funktionen angewendet werden, separat zu Funktionen organisiert werden. Mit der objektorientierten Programmierung wurde das Objekt als zentrales Element zur Organisation von Quellcode eingeführt. Objekte besitzen Eigenschaften und Methoden, welche somit Daten und Funktionen zur Manipulation der Daten zusammenführen.
In Python ist alles ein Objekt und Python ist damit eine objektorientierte Programmiersprache. Python besitzt den Dot-Operator, um auf die Methoden und Eigenschaften eines Objektes zuzugreifen.
Sprachliche Verwirrung
Eine Funktion ist generell identisch mit einer Methode, nur das die Methode innerhalb eines Klassenkontextes definiert ist. Als Eigenschaften, Attribute oder Felder werden Variablen im Klassenkontext bezeichnet.
Die Grundprinzipien der Objektorientierten Programmierung manifestieren sich anhand von 3 Konzepten:
- Datenkapselung
- Vererbung
- Polymorphismus
Beispiel: String Objekt
Ein String ist ein Objekt und bietet unterschiedliche nützliche Methoden zur Manipulation des Strings. Die Methode wird dabei über den Dot-Operator am String-Objekt aufgerufen.
Als Beispiel soll der String "hello" in Großbuchstaben umgewandelt werden. Strings bieten die Methode upper um dies durchzuführen:
a = "hello"
print(a.upper()) # HELLO
Die Zeile a = "hello" erzeugt ein Objekt vom Typ String und hinterlegt das Objekt in der Variable a. Mit dem Dot-Operator können alle definierten Methoden des Strings aufgerufen werden. a.upper() ruft also die Methode upper auf dem String "hello" auf.
String Methoden
Strings in Python besitzen eine Vielzahl hilfreicher Methoden. Eine Übersicht der Methoden findet sich zum Beispiel hier.
Klassen
Einen Klasse definiert einen neuen Typ (oder Datentyp) in Python. Klassen werden in der Softwareentwicklung gerne mit Bauplänen verglichen. Ein Objekt ist dann als konkrete Ausprägung oder Instanz einer Klasse zu verstehen. Aus einer Klasse können also beliebig viele Objekte erzeugt werden.
Die Konstruktion eines Objektes auf Basis einer Klasse wird als Instanziierung bezeichnet. Jede Klasse kann eine Konstruktor Funktion definieren um den Vorgang der Instanziierung zu steuern.
Klassen definieren die Eigenschaften und Methoden von Objekten. Die Eigenschaften und Methoden werden über den Dot-Operator angesprochen.
Beispiel: Klasse Vehicle
In Python wird das Schlüsselwort class verwendet um eine Klasse zu definieren. Die Bezeichnung einer Klasse sollte den Regeln von PEP8 folgen und mit großem Buchstaben beginnend, als Camel Case geführt werden.
class Vehicle:
def drive(self):
print("I am driving")
veh = Vehicle()
veh.drive() # print: I am driving
self
Die Klasse stellt den Bauplan für Objekte (aka Instanzen der Klasse) dar. Jede Methode hat als ersten Parameter self definiert. self ist immer eine Referenz auf die Instanz, auf welcher die Methode aufgerufen wird. Beim Methodenaufruf an der Instanz muss der Parameter self nicht gesetzt werden, dieser wird implizit gesetzt. Am Beispiel veh.drive() wird dies ersichtlich.
Klassendefinition mit Konstruktor
In Python werden Methoden, welche mit __ beginnen und mit __ enden, als sog. Magic Methods bezeichnet. Diese Magic Methods haben eine gesonderte Bedeutung. Der Konstruktur in der Klassendefinition __init__ ist eine Magic Methode mit besonderer Bedeutung. Ein Konstruktur kann implementiert werden um die Erzeugung eines Objektes gesondert zu steuern.
Im Beispiel erhält der Konstruktor einen Parameter name, welcher in der Methodenimplementierung als Eigenschaft des Objektes gesetzt wird. Die Eigenschaft name wird im Aufruf der Methode drive verwendet um die entsprechende Ausgabe zu erzeugen.
class Vehicle:
def __init__(self, name):
self.name = name
def drive(self):
print("{} is driving".format(self.name))
audi = Vehicle("Audi A4")
puch = Vehicle("Puch Maxi")
audi.drive() # print: Audi A4 is driving
puch.drive() # print: Puch Maxi is driving
Eigenschaften
Es wird generell zwischen Klasseneigenschaften und Objekteigenschaften unterschieden. Klasseneigenschaften sind innerhalb der Klasse definiert und mit allen Instanzen der Klasse geteilt. Im Gegensatz dazu sind die Objekteigenschaften für jede Instanz eigenständig definiert.
Des Weiteren wird die Sichtbarkeit von Eigenschaften definiert. Die Sichtbarkeit bestimmt wie der Zugriff auf die Eigenschaften von Außen (außerhalb des Objekt- oder Klassenkontextes) geregelt ist. Public Eigenschaften können von Außen gelesen und verändert werden. Private Eigenschaften sind nach Außen weder lesbar noch veränderbar.
Beispiel: Public Eigenschaften
Die Klasse Vehicle definiert die öffentliche Eigenschaft production_count als Klasseneigenschaft. Mit dem Dot-Operator und dem Klassennamen Vehicle.production_count kann auf diese Variable an jeder Stelle lesend und schreibend zugegriffen werden. Die Eigenschaft wird mit 0 initialisiert und innerhalb jedes Konstrukturaufrufs um 1 erhöht. Die Klasseneigenschaft enhält also die Anzahl aller Instanzen der Klasse Vehicle.
Die Eigenschaft name wird innerhalb des Konstruktors als öffentliche Objekteigenschaft definiert. Der Parameter name des Konstrukturs wird der Objekteigenschaft zugewiesen.
class Vehicle:
production_count = 0
def __init__(self, name):
self.name = name
Vehicle.production_count += 1
v1 = Vehicle("Audi")
v2 = Vehicle("BMW")
v3 = Vehicle("Tesla")
print("Count: ", Vehicle.production_count) # Count: 3
print("Vehicles: ", v1.name, v2.name, v3.name) # Vehicles: Audi BMW Tesla
Beispiel: Private Eigenschaften
Die Klasse Vehicle definiert im Konstruktur eine private Objekteigenschaft __name. Durch die führenden 2 Unterstriche wird die Eigenschaft als private definiert. Ein Zugriff außerhalb des Objektkontexts ist damit nicht mehr möglich. Die Methode get_name wurde eingeführt um lesenden Zugriff auf die Eigenschaft zu gewährleisten.
class Vehicle:
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
v = Vehicle("Audi")
print("Vehicle name: ", v.get_name()) # Vehicle name: Audi
Verständnisfrage
Versuchen Sie die Eigenschaft v.__name auf zu rufen. Was passiert?
UML
Die Unified Modeling Language (UML) ist eine standardisierte Modellierungssprache um vorranging objektorientierte Softwaresysteme zu entwerfen oder zu dokumentieren. Zur Modellierung werden Diagramme verwendet. UML umfasst eine Menge von Diagrammen, welche für die Beschreibung der Struktur bzw. des Verhaltens eines Softwaresystems verwendet werden können.
Im Kontext dieser LV blicken wir ausschließlich auf das Klassendiagramm. Mit dem Klassendiagramm kann schnell ein Überblick über ein komplexes Softwaresystem gegeben werden. Der Kernbestandteil des Klassendiagramms ist natürlich die Klasse, dessen Darstellung im nächsten Abschnitt erörtert wird.
Klassen
Klassen werden als Rechteck modelliert und in 3 Abschnitte gegliedert: (1) Klassenname, (2) Eigenschaften und (3) Methoden.
Für Eigenschaften und Methoden der Klasse ist auch die Sichtbarkeit definiert. Dazu werden die Symbole +, - bzw. # verwendet. In der Tabelle sind Details zur Sichtbarkeit aufgeführt.
| Symbol | Bezeichner | Beschreibung |
|---|---|---|
+ |
public | nach Außen lesbar bzw. bearbeitbar |
- |
private | kann nur innerhalb der Klasse gelesen und verändert werden |
# |
protected | kann nur innerhalb der Vererbungshierarchie gelesen bzw. verändert werden (nicht unterstützt in Python) |
Als Beispiel wird die Klasse Vehicle als UML-Klasse definiert. Die Klasse besitzt die private Eigenschaft name bzw. die 2 public Methoden drive und break:
Die UML Klasse würde in Etwa folgendem Python Quellcode entsprechen:
class Vehicle:
def __init__(self, name):
self.__name = name
def drive():
pass
def break():
pass
Quellcode vs UML
UML Diagramme sind grafische Repräsentation von Programmen. Das Ziel von UML ist es komplexe Zusammenhänge vereinfacht darzustellen. Durch die grafische Repräsentation gibt es Details, welche nicht modelliert werden und somit ist eine 1 zu 1 Überführung von UML in Quellcode nicht möglich.
Beziehungen zwischen Klassen bzw. Objekten
Zur Realisierung eines Softwaresystems müssen Klassen bzw. Objekte in Beziehung gebracht werden. Beispielsweise soll eine Instanz der Klasse Vehicle mit mehreren Instanzen der Klasse Tire bestückt werden können.
class Vehicle:
def __init__(self, name):
self.__name = name
self.__tires = []
def add_tire(self, tire):
self.__tires.append(tire)
class Tire:
def __init__(self, pressure):
self.__pressure = pressure
vehicle = Vehicle("mymotorbike")
vehicle.add_tire(Tire(200))
vehicle.add_tire(Tire(210))
Eine Instanz der Klasse Vehicle enthält die private Eigenschaft tires, welche eine geordnete Menge von Instanzen der Klasse Tire enthalten kann. Ein wichtiger Begriff in diesem Zusammenhang ist die Navigierbarkeit. Die Navigierbarkkeit zwischen Vehicle und Tire ist im obigen Beispiel unidirektional. Die Klasse Vehicle hat Zugriff auf die Eigenschaft tires und dadurch Zugriff auf die verwalteten Tire Objekte. Ein Tire hat keinen Zugriff auf das Vehicle mit dem es verknüpft ist.
Eine bidirektionale Beziehung zwischen den beiden Klassen, müsste eine Verknüpfung von Tire zu Vehicle ermöglichen. Im folgenden Beispiel wird die Klasse Tire um die Methode add_vehicle erweitert. Diese Methode ermöglicht eine navigierbare Verknüpfung mit Vehicle.
class Vehicle:
def __init__(self, name):
self.__name = name
self.__tires = []
def add_tire(self, tire):
self.__tires.append(tire)
tire.add_vehicle(self)
class Tire:
def __init__(self, pressure):
self.__pressure = pressure
self.__vehicle = None
def add_vehicle(self, vehicle):
self.__vehicle = vehicle
vehicle = Vehicle("mymotorbike")
vehicle.add_tire(Tire(200))
vehicle.add_tire(Tire(210))
UML: Beziehung
Beziehungen können auch innerhalb eines UML Klassendiagrammes ausgedrückt werden. Im Bild wird die Beziehung zwischen Vehicle und Tire dargestellt. Die Pfeilspitze zeigt von Vehicle auf Tire um die unidirektionale Navigierbarkeit darzustellen.
Auch eine bidirektionale Beziehung zwischen Vehicle und Tire kann mit UML abgebildet werden. Dazu müssen die Klassen ohne Pfeilspitzen verknüpft werden:
Klassenvererbung
Aus Gründen der Wiederverwendbarkeit können Klassenbestandteile, also Eigenschaften und Methoden, von Basisklassen auf abgeleitete Klassen vererbt werden. Gemeinsamkeiten können so übernommen werden und müssen nicht erneut implementiert werden. Innerhalb der abgeleiteten Klasse können Methoden der Basisklasse jedoch überschrieben und neu implementiert werden.
Im folgenden Beispiel werden die Klassen Vehicle und Bicycle definiert. In der Klassendefinition von Bicycle wird dahinter in Klammern (Vehicle) angegeben. Dies bedeutet, dass die Klasse Bicycle von Vehicle erbt und somit alle Methoden von Vehicle übernimmt:
class Vehicle:
def drive(self):
print("drive...")
class Bicycle(Vehicle):
pass
bike = Bicycle()
bike.drive() # print: drive...
Die Klasse Bicycle kann die Methode drive der Basisklasse überschreiben und eigens implementieren:
class Vehicle:
def drive(self):
print("drive...")
class Bicycle(Vehicle):
def drive(self):
print("pedaling...")
bike = Bicycle()
bike.drive() # print: pedaling...
Eine Klassenhierarchie kann beliebig tief sein. Im folgenden Beispiel wird die Klasse EBike eingeführt, welche von Bicycle erbt. Somit herrscht eine dreistufige Vererbungshierarchie vor EBike > Bicycle > Vehicle. Des Weiteren gibt es die spezielle Funktion super. Mit dieser Funktion kann auf die nächst höhere Instanz der Vererbungshierarchie zugegriffen werden. Dies ist vorallem nützlich, wenn eine Methode in der abgeleiteten Klasse überschrieben wird. Mit super kann die überschriebene Methode aufgerufen werden.
class Vehicle:
def drive(self):
print("drive...")
class Bicycle(Vehicle):
def drive(self):
print("pedaling...")
class EBike(Bicycle):
def drive(self):
super().drive()
print("+ support with e-motor")
bike = EBike()
bike.drive()
# print:
# pedaling...
# + support with e-motor
Der Aufruf der Methode drive auf der Instanz von EBike führt dazu, dass (1) die Methode drive der Klasse Bicycle aufgerufen wird und (2) die Ausgabe der Methode drive der Klasse EBike durchgeführt wird. Das Aufrufen der überschriebenen Methode der Basisklasse wird über die Funktion super realisiert.
UML: Klassenvererbung
Mit der Vererbungsbeziehung kann eine Klassenhierarchie in UML modelliert werden. Die Verbung wird im UML Klassendiagramm über einen nicht ausgefüllten Pfeil dargestellt. Die Pfeilspitze ist dabei immer auf die Basis- bzw. Elternklasse gerichtet.
Beispiel: Bankkonto
An einem kleinen Beispiel eines Bankkontos soll der Unterschied der prozeduralen und objektorientieren Herangehensweise demonstriert werden.
Das Beispiel soll im Wesentlichen verdeutlichen wie Daten und Operationen auf den Daten innerhalb einer Klasse gekapselt werden.
Prozedurales Bankkonto
account_elon = {
"owner":"ElonMillionär",
"number":5671,
"balance":19000000
}
def deposit(account, amount):
account["balance"] += amount
def withdraw(account, amount):
account["balance"] -= amount
deposit(account_elon, 100)
withdraw(account_elon, 400)
Objektorientiertes Bankkonto
class Account:
def __init__(self, owner, number, balance):
self.owner = owner
self.number = number
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount
account_elon = Account("Elon Millionär", 5671, 19000000)
account_elon.deposit(100)
account_elon.withdraw(400)
Polymorphismus
Im Kontext der OOP wird unter Polymorphismus verstanden, dass es vielgestaltige Implementierungen auf Basis einer einheitlichen Schnittstellendefinition geben kann. Um dies zu verdeutlichen soll ein Beispiel aus dem Alltag herangezogen werden. Es gibt die beispielsweise den E27 Standard für eine Leuchtmittelfassung. Es gibt unterschiedlichste Arten von Leuchtmitteln, welche aber für den Stromanschluss alle der definierten E27 Schnittstelle entsprechen:

Beispiel mit der Programmiersprache Java
Anders als Python ist Java statisch Typisiert. Dies bedeutet eine Variable wird mit einem Datentyp initialisiert und dieser kann im Programmverlauf nicht mehr verändert werden. Java bietet auch das Konzept des interface, welches eine Schnittstellendefinition für Klassen definiert.
Die Schnittstelle Flyable definiert die Methode fly, alle Klassen, welche diese Schnittstelle implementieren, müssen diese Methode ebenfalls implementieren. So wird in Java statisch festgelegt, dass gewisse Methoden existieren.
interface Flyable {
public void fly();
}
class Duck implements Flyable {
public void fly() {
System.out.println("Duck is flying");
}
}
class Airplane implements Flyable {
public void fly() {
System.out.println("Airplane is flying");
}
}
Um nun zu demonstrieren was unter Polymorphismus verstanden wird, soll eine Klasse Client erzeugt werden, welche eine Methode letITFly besitzt. Die Methode letItFly hat einen Parameter definiert, welcher vom Typ Flyable ist. Das bedeutet, dass der übergebene Parameter der Schnittstelle Flyable entsprechen muss.
Die beiden Implementierungen von Flyable, also Airplane und Duck können also als Parameter für die Methode letItFly herangezogen werden.
class Client {
public static void letItFly(Flyable flyable) {
flyable.fly();
}
}
Client.letItFly(new Airplane()); // Airplane is flying
Client.letItFly(new Duck()); // Duck is flying
Mit der Definition einer Schnittstelle (aka interface) in Java wird also die Vielgestaltigkeit möglicher Implementierungen realisiert. Der Aufruf der Methode fly an der Schnittstelle wird an die entsprechenden Implementierungen weitergereicht und führt zu unterschiedlichem Verhalten.
Beispiel mit der Programmiersprache Python
Python ist dynamisch Typisiert und Variablen können während der Laufzeit den Datentyp verändern. In Python gibt es keinen strengen Mechanismus um festzulegen, welche Methoden eine Klasse aufweisen muss. Deshalb manifestiert sich die Idee des Polymorphismus in Python an der reinen Existenz von Methoden innerhalb der Klasse. Dies ist wird auch unter dem Begriff des Duck Typings verstanden.
It walks like a duck, it quacks like a duck => it is a duck
class Duck:
def fly(self):
print("Duck is flying")
class Airplane:
def fly(self):
print("Airplane is flying")
Aufgrund der dynamischen Typisierung ist für die Voraussetzung des Polymorphismus nur die Existenz der Methode fly zwingend:
class Client:
def let_it_fly(self, flyable):
flyable.fly()
client = Client()
client.let_it_fly(Duck()) # Duck isflying
client.let_it_fly(Airplane()) # Airplane is flyin