Home | Lehre | Videos | Texte | Vorträge | Software | Person | Impressum, Datenschutzerklärung |
Stand: 2024-05-19
weitgehend formuliert von ChatGPT 4
Objektorientierte Programmierung (OOP) ist ein
Programmierparadigma
, also eine Herangehensweise zum Schreiben
und Organisieren von Programmcode. Es zielt darauf ab, Code in einer
strukturierten und intuitiven Weise zu organisieren, indem Daten und
Funktionen in Einheiten, sogenannten Objekten, zusammengefasst werden.
Im Gegensatz zum prozeduralen Programmierparadigma, wie es oft in C
verwendet wird, wo Daten und Funktionen getrennt sind, ermöglicht OOP,
dass Daten und die Funktionen, die sie manipulieren, in denselben
Einheiten enthalten sind. Diese Objekte sind wie kleine, selbstständige
Maschinen, die bestimmte Daten speichern und Operationen auf diesen
Daten ausführen können.
Dieser Ansatz hilft dabei, Programme modularer und klarer zu strukturieren. Insbesondere kann man Objekten Anweisungen geben wie in diesem Beispiel (der komplette Code folgt weiter unten):
# Erzeugung eines Kaffeemaschinen-Objekts
= Kaffeemaschine('EspressoMaster', 2)
meine_kaffeemaschine
# Nutzung des Kaffeemaschinen-Objekts
meine_kaffeemaschine.einschalten()1) # Füge 1 Liter Wasser hinzu
meine_kaffeemaschine.wasser_auffüllen(# Brühe eine Tasse Kaffee
meine_kaffeemaschine.kaffee_brühen() meine_kaffeemaschine.ausschalten()
Ein zentraler Aspekt der OOP ist die Kapselung,
welche die Zusammenfassung von Daten (in diesem Zusammenhang Attribute
genannt) und Funktionen (in diesem Zusammenhang Methoden genannt)
innerhalb von Objekten sowie den Schutz dieser Daten vor unerlaubtem
Zugriff von außen umfasst. Kapselung ermöglicht es, dass Objekte ihre
internen Daten verbergen (man spricht von Datenkapselung
) und nur
über definierte Schnittstellen (Methoden) angesprochen werden, fast als
ob man von einer komplexen Maschine nur ein paar Taster sieht, aber
nicht an das Innenleben gelangt. Dies erhöht die Sicherheit und
Robustheit des Code, da Fehlerquellen reduziert werden, indem verhindert
wird, dass externe Funktionen oder andere Objekte direkt auf interne
Datenstrukturen zugreifen. In Python ist die Kapselung allerdings nicht
streng wie in Sprachen wie C++ und Java, sondern eher ein Vorschlag.
Ein weiterer Grundbegriff der OOP ist die Unterscheidung
zwischen Klassen und Objekten = Instanzen. Eine Klasse ist eine
Blaupause oder Schablone, die die Struktur (Daten, in diesem
Zusammenhang Attribute genannt) und das Verhalten (Funktionen, in diesem
Zusammenhang Methoden genannt) der Objekte definiert, die von ihr
erzeugt werden. Ein Objekt ist eine konkrete Ausprägung dieser Klasse,
man nennt das eine Instanz
der Klasse; die Klasse wird
instanziiert
.
Beispielsweise könnte in Python eine Klasse Auto
Attribute (also Daten) wie farbe
und
kennzeichen
sowie Methoden (also Funktionen) wie
fahren()
und bremsen()
haben. Ein Auto-Objekt
ist dann eine Instanz dieser Klasse, zum Beispiel mit dem konkreten
Autokennzeichen BI XY 1234 E
.
In der fortgeschritteneren Programmierung gibt man auch einer Klasse
selbst (statt ihren einzelnen Instanzen) Attribute und Methoden. Diese
heißen dann statisch
. Beispiele dafür in einer Klasse
Auto
sind die für alle Autos der Klasse gleiche und
unveränderliche Zahl der Türen oder die Zahl bisher erzeugter Instanzen
von Auto
.
Vererbung ist ein weiteres wichtiges Konzept in der OOP, das es Klassen ermöglicht, Attribute und Methoden von einer anderen Klasse zu übernehmen. Dies unterstützt die Wiederverwendung von Code und die Erstellung von weit verzweigten Stammbäumen von Klassen. Wenn eine Klasse von einer anderen erbt, wird die erstere als Unterklasse oder abgeleitete Klasse oder Kindklasse und die letztere als Oberklasse oder Basisklasse oder Mutterklasse bezeichnet.
Ein äußerst wichtiger praktischer Aspekt der Vererbung ist die
Wiederverwendung von Code durch die Nutzung von vorgefertigten
Klassenbibliotheken: Man leitet von deren Klassen eigene Klassen ab und
erbt damit eine umfangreiche Funktionalität. Dies beschleunigt die
Entwicklung erheblich und reduziert Fehler, da diese Bibliotheken in der
Regel gut getestet sind. Wir werden insbesondere die für grafische
Bedienoberflächen gedachte Bibliothek PySide
als ein
Beispiel für Klassenbibliotheken kennenlernen.
Polymorphie (Vielgestaltigkeit
) ist ein
Konzept in der objektorientierten Programmierung, das es erlaubt,
Methoden mit dem gleichen Namen in verschiedenen Klassen zu verwenden.
Diese Methoden können unterschiedliche Aktionen ausführen, je nachdem,
zu welcher Klasse das Objekt gehört, das sie aufruft. Das ermöglicht es,
Objekte unterschiedlicher Klassen auf die gleiche Weise zu behandeln,
was den Code flexibler und leichter erweiterbar macht. Dies wird
insbesondere in Klassenbibliotheken ausgenutzt.
Stellen Sie sich Polymorphie wie eine Zirkusvorstellung vor, bei der
jedes Tier aufgefordert wird, einen Ton zu machen. Die Aktion Ton
machen
ist für alle gleich, aber jedes Tier macht einen anderen
Ton:
Wuffmachen.
Miausagen.
Hier sagt der Name der Aktion Ton machen
nichts darüber aus,
welches spezifische Geräusch gemacht wird. Welches Geräusch herauskommt,
hängt davon ab, um welches Tier es sich handelt. In der Programmierung
ermöglicht Polymorphie genau das: Sie definieren eine Methode (wie
Ton machen
) in einer Basisklasse (wie Tier
), und jede
Unterklasse (wie Hund
, Katze
oder Vogel
) kann diese
Methode auf ihre eigene Weise ausführen. Die jeweiligen Klassen
überschreiben
dann die Methode der Oberklasse. (Auf Englisch
heißt das to override, bezeichnet also eher,
dass eine höhere Instanz eine Entscheidung einer niedrigeren Instanz
außer Kraft setzt.)
Im Extremfall führt man für die Polymorphie eine Oberklasse ein, die
so allgemein ist, dass man die Methoden gar nicht ausformulieren kann,
eine abstrakte Klasse
. Im Beispiel oben könnte das die Klasse
Tier
sein. Erst die davon erbenenden Unterklassen
Hund
usw. haben dann konkrete Methoden.
Es ist eine Klasse Kaffeemaschine
zu schreiben, damit
dieser oben schon erwähnte Code zum Anwenden der Kaffeemaschine
funktioniert:
# Erzeugung eines Kaffeemaschinen-Objekts
= Kaffeemaschine('EspressoMaster', 2)
meine_kaffeemaschine
# Nutzung des Kaffeemaschinen-Objekts
meine_kaffeemaschine.einschalten()1) # Füge 1 Liter Wasser hinzu
meine_kaffeemaschine.wasser_auffüllen(# Brühe eine Tasse Kaffee
meine_kaffeemaschine.kaffee_brauen() meine_kaffeemaschine.ausschalten()
Zuerst definieren wir eine Klasse Kaffeemaschine
. Eine
Klasse in ist wie ein Bauplan für das Erstellen von Objekten
(Instanzen). Jedes Objekt, das von dieser Klasse erstellt wird, hat die
in der Klasse definierten Eigenschaften (Attribute = Daten) und Methoden
(Funktionen).
class Kaffeemaschine:
'''
Repräsentiert eine Kaffeemaschine mit einstellbarem Wassertank und der Fähigkeit, Kaffee zu brühen.
'''
Sämtliche Bestandteile der Klasse stehen dann eingerückt (diese Einrückung nicht vergessen!) in den Zeilen darunter.
__init__
Innerhalb der Klasse definieren wir die Initialisierungsfunktion
__init__
(geschrieben mit jeweils zwei Unterstrichen vorne
und hinten). Diese Funktion wird immer aufgerufen, wenn ein neues Objekt
der Klasse erstellt wird. (Technische Anmerkung: __init__
macht, was in anderen Sprachen der Konstruktor
einer Klasse
macht. In Python gibt es aber obendrein einen echten Konstruktor
__new__
. Deshalb für __init__
ist die
Bezeichnung Initialisierungsfunktion
besser als
Konstruktor
.)
def __init__(self, marke: str, wassertank_kapazität: float):
'''
Initialisiert eine neue Kaffeemaschine mit einer Marke und einer Wassertankkapazität.
Args:
marke (str): Die Marke der Kaffeemaschine.
wassertank_kapazität (float): Die Kapazität des Wassertanks in Litern.
Diese Methode setzt den anfänglichen Füllstand des Wassertanks auf 0 Liter und
initialisiert die Maschine in ausgeschaltetem Zustand.
'''
self.marke: str = marke # Marke der Kaffeemaschine als String
self.wassertank_kapazität: float = wassertank_kapazität # Maximale Kapazität des Wassertanks in Litern
self.wassertank_füllstand: float = 0.0 # Aktueller Füllstand des Wassertanks in Litern, startet bei 0
self.ist_eingeschaltet: bool = False # Status der Kaffeemaschine, True wenn eingeschaltet, sonst False
Die Variable self
verweist auf die Instanz des Objekts
selbst. Anders gesagt, self
ist ein Referenzpunkt zu dem
Objekt, das gerade verwendet oder manipuliert wird. Mittels
self
kann man in der Methoden der Klasse auf deren
Bestandteile wie Daten (Attribute) und Funktionen (Methoden)
zugreifen.
Die Typangaben wie str
und float
in diesem
Code sind wieder freiwillig, aber sehr empfehlenswert. Die
Initialisierungsfunktion bekommt keinen Rückgabetyp
-> irgendein_Typ
, denn sie darf auch nicht mit
return irgendwas
ein Ergebnis liefern, sondern darf nur am
halbfertigen Objekt (das sie in der Variablen self
erhält)
weiterbasteln.
Das Setzen von self.marke = irgendwas
bewirkt, dass
diese Variable (dieses Attribut) erstens in der gerade behandelten
Instanz angelegt wird und zweitens auf einen bestimmten Wert gesetzt
wird. Entsprechendes gilt für die weiteren drei Attribute.
einschalten
und ausschalten
Die Methode einschalten
setzt den Status der
Kaffeemaschine auf eingeschaltet (True
). Diese Methode
ändert den Zustand von ist_eingeschaltet
auf
True
und gibt eine Nachricht aus.
def einschalten(self) -> None:
'''Schaltet die Kaffeemaschine ein.'''
self.ist_eingeschaltet = True
print(f'{self.marke} Kaffeemaschine eingeschaltet.')
Die Methode ausschalten
arbeitet umgekehrt:
def ausschalten(self) -> None:
'''Schaltet die Kaffeemaschine aus.'''
self.ist_eingeschaltet = False
print(f'{self.marke} Kaffeemaschine ausgeschaltet.')
Wie können Sie beide Methoden zu einer einzigen zusammenfassen?
wasser_auffüllen
Diese Methode fügt Wasser zum Wassertank hinzu. Sie erhält die Menge des Wassers als Parameter und prüft, ob nach dem Hinzufügen der Menge der Füllstand den Tank überlaufen würde. Wenn ja, gibt sie eine Fehlermeldung aus; wenn nein, füllt sie den Tank und gibt den neuen Füllstand auf der Kommandozeile aus (aber nicht als Rückgabewert!).
def wasser_auffüllen(self, menge: float) -> None:
'''
Füllt den Wassertank um die angegebene Menge auf.
Args:
menge (float): Die Menge an Wasser in Litern, die hinzugefügt werden soll.
'''
if self.wassertank_füllstand + menge <= self.wassertank_kapazität:
self.wassertank_füllstand += menge
print(f"Wassertank aufgefüllt. Aktueller Füllstand: {self.wassertank_füllstand} Liter.")
else:
print("Fehler: Füllen abgelehnt, weil der Tank überlaufen würde!")
kaffee_brühen
Diese Methode versucht, eine Tasse Kaffee zu brühen. Sie prüft zunächst, ob die Maschine eingeschaltet ist und genügend Wasser im Tank vorhanden ist. Wenn beides zutrifft, braut sie eine Tasse Kaffee, verbraucht dabei 0,25 Liter Wasser und gibt eine Erfolgsmeldung aus. Wenn nicht, gibt sie eine entsprechende Fehlermeldung aus.
def kaffee_brühen(self):
'''
Brüht eine Tasse Kaffee, wenn genügend Wasser vorhanden ist und die Maschine eingeschaltet ist.
'''
if self.ist_eingeschaltet:
if self.wassertank_füllstand >= 0.25:
self.wassertank_füllstand -= 0.25 # Verbraucht 0,25 Liter Wasser pro Tasse
print('Kaffee wird gebrüht. Eine Tasse fertig!')
else:
print('Fehler: Nicht genug Wasser.')
else:
print('Fehler: Kaffeemaschine ist ausgeschaltet.')
Außerhalb der Klassensdefinition, also nicht mehr eingerückt,
erstellen wir nun eine Instanz der Kaffeemaschine
und
speichern sie (genauer: eine Referenz auf sie; mehr dazu später) in
einer Variable namens meine_kaffeemaschine
. Wir nutzen die
Initialisierungsfunktion __init__
und geben ihr die
notwendigen Parameter:
= Kaffeemaschine('EspressoMaster', 2) meine_kaffeemaschine: Kaffeemaschine
(In Sprachen wie C++ und Java steht an dieser Stelle das Schüsselwort
new
. Nicht so in Python.)
Wie man sieht, ist der Typ der Variablen
meine_kaffeemaschine
die Klasse
Kaffeemaschine
. Jede Instanz von
Kaffeemaschine
enthält einen str
, zwei
float
, einen bool
und kann Methoden wie
einschalten
ausführen. Zusammengenommen muss das ein ganz
anderer Typ sein als etwa str
oder int
.
Nun benutzen wir die Methoden der Kaffeemaschine, um sie
einzuschalten, Wasser aufzufüllen und Kaffee zu brühen. Die Mühe, vorher
die Klasse Kaffeemaschine
zu definieren, zahlt sich nun
aus, weil man schnell und klar schreiben kann:
meine_kaffeemaschine.einschalten()1) # Füge 1 Liter Wasser hinzu
meine_kaffeemaschine.wasser_auffüllen(# Brühe eine Tasse Kaffee
meine_kaffeemaschine.kaffee_brühen() meine_kaffeemaschine.ausschalten()
Beachte, dass erst an dieser Stelle wirklich etwas passiert. Die Klassendefinition sagt als Plan, was passieren soll, aber erst hier werden die Methoden wirklich aufgerufen.
self
In der obigen Definition der Klasse taucht an vielen Stellen die
Variable self
auf, aber nicht beim Verwenden der
Klasse, zum Beispiel nicht bei
meine_kaffeemaschine.wasser_auffüllen(1)
. Die Methoden
einer Instanz haben in der Definition der Klasse alle einen Parameter
mehr, als man erwarten würde: self
. Dieses
self
verweist auf das jeweils aktuelle Objekt, auf das die
Definition der Klasse angewendet wird.
Die Python-Laufzeitumgebung macht dazu hinter den Kulissen ungefähr
Folgendes: Wenn man
meine_kaffeemaschine.wasser_auffüllen(1)
aufruft,
verwandelt sie dies in
wasser_auffüllen(meine_kaffeemaschine, 1)
. Auf diese Weise
landet das in der Variablen meine_kaffeemaschine
angegebene
Objekt im Parameter self
der Methode.
(In Sprachen wie C++ und Java gibt es statt der Variablen
self
das Schüsselwort this
. Es ist dort beim
Definieren der Methoden einer Klasse direkt verfügbar, ohne dass man es
als Parameter angibt. Außerdem kann es dort weggelassen werden, wenn es
keine Mehrdeutigkeiten gibt. Das self
von Python ist
dagegen immer anzugeben, um in der Definition der Klasse auf
Attribute und Methoden der Instanz zuzugreifen.)
OOP bringt einige Vokabeln mit sich, die in der Fachsprache der Informatik völlig andere Bedeutungen haben als im Alltag. Im Laufe des Semesters sollten Sie lernen, die folgenden Vokabeln selbst zu verwenden:
class
, Objekt, Instanz, instanziierenself
__init__
(strenggenommen kein
Konstruktor)super()
@staticmethod
,
@classmethod