Home | Lehre | Videos | Texte | Vorträge | Software | Person | Impressum, Datenschutzerklärung | Blog RSS

Stand: 2024-05-19
weitgehend formuliert von ChatGPT 4

Objektorientierte Programmierung, Teil 1

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
meine_kaffeemaschine = Kaffeemaschine('EspressoMaster', 2)

# Nutzung des Kaffeemaschinen-Objekts
meine_kaffeemaschine.einschalten()
meine_kaffeemaschine.wasser_auffüllen(1)  # Füge 1 Liter Wasser hinzu
meine_kaffeemaschine.kaffee_brühen()  # Brühe eine Tasse Kaffee
meine_kaffeemaschine.ausschalten()

Grundkonzepte der Objektorientieren Programmierung

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:

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.

Ein Beispiel für Objektorientierung in Python

Es ist eine Klasse Kaffeemaschine zu schreiben, damit dieser oben schon erwähnte Code zum Anwenden der Kaffeemaschine funktioniert:

# Erzeugung eines Kaffeemaschinen-Objekts
meine_kaffeemaschine = Kaffeemaschine('EspressoMaster', 2)

# Nutzung des Kaffeemaschinen-Objekts
meine_kaffeemaschine.einschalten()
meine_kaffeemaschine.wasser_auffüllen(1)  # Füge 1 Liter Wasser hinzu
meine_kaffeemaschine.kaffee_brauen()  # Brühe eine Tasse Kaffee
meine_kaffeemaschine.ausschalten()

Definition der Klasse

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.

Initialisierungsfunktion __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.

Methoden 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?

Methode 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!")

Methode 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.')

Erzeugen und Verwenden einer Kaffeemaschinen-Instanz

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:

meine_kaffeemaschine: Kaffeemaschine = Kaffeemaschine('EspressoMaster', 2)

(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()
meine_kaffeemaschine.wasser_auffüllen(1)  # Füge 1 Liter Wasser hinzu
meine_kaffeemaschine.kaffee_brühen()  # Brühe eine Tasse Kaffee
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.

Die Variable 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.)

Vokabular der Objektorientierung

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: