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

Stand: 2024-06-11
weitgehend formuliert von ChatGPT-4o

Abstrakte Klassen

Abstrakte Klassen helfen uns dabei, eine Grundstruktur für andere Klassen zu erstellen. Sie legen fest, welche Methoden (Funktionen) die abgeleiteten Klassen unbedingt haben müssen. Diese abstrakten Klassen selbst können nicht direkt benutzt werden, sondern dienen als Vorlage für andere Klassen.

Eine abstrakte Klasse ist damit eine Art Bauplan für andere Klassen. Normale, also konkrete Klassen sind dagegen Baupläne für Instanzen, also Objekte. Eine abstrakte Klasse sagt, welche Methoden die abgeleiteten Klassen haben müssen, gibt aber noch nicht an, was genau diese Methoden wie tun. Diese konkreten Details werden erst in abgeleiteten Klassen festgelegt.

Beispiel einer abstrakten Klasse

Eine abstrakte Klasse verwendet man dann, wenn eine konkrete Klasse an ihrer Stelle auf unlogische Art zu allgemein wäre. Hier nehmen wir als Beispiel eine geometrische Form, zum Beispiel für ein Zeichenprogramm. Es gibt keine allgemeine, abstrakte geometrische Form auf dem Bildschirm, sondern nur konkrete Formen: Vierecke, Kreise usw. Aber dennoch ist die abstrakte geometrische Form hilfreich, um festzulegen, was alle konkreten geometrischen Formen können sollen.

Zum Beispiel möchten wir sicherstellen, dass jede Form ihre Fläche berechnen kann. Das können wir mit einer abstrakten Klasse sicherstellen, von der dann alle konkreten Formen erben:

from abc import ABC, abstractmethod

class Form(ABC):
    @abstractmethod
    def berechne_Fläche(self):
        pass

Diese Klasse Form erbt ihrerseits von der vordefinierten Elternklasse ABC (das ABC steht für Abstract Base Class; jemand fand diese Abkürzung wohl witzig). Das kennzeichnet sie als abstrakte Klasse.

In dieser abstrakten Klasse Form haben wir eine abstrakte Methode berechne_Fläche definiert, die dann in allen abgeleiteten Klassen vorhanden sein muss. Diese Methode ist abstrakt, wird also hier in der abstrakten Klasse nicht implementiert (d.h. ausprogrammiert), sondern nur festgelegt. Es würde auch keinen Sinn ergeben, die Fläche der allgemeinen Form zu bestimmen, denn was sollte man hier rechnen?

Die Implementation der Methode berechne_Fläche erfolgt dann in den konkreten Kindklassen dieser abstrakten Klasse:

import math

class Kreis(Form):
    def __init__(self, radius):
        self.radius = radius

    def berechne_Fläche(self):
        return math.pi * self.radius ** 2

class Rechteck(Form):
    def __init__(self, breite, höhe):
        self.breite = breite
        self.höhe = höhe

    def berechne_Fläche(self):
        return self.breite * self.höhe

Wir können sicher sein, dass alle Instanzen der konkreten Klassen Kreis und Rechteck die Methode berechne_Fläche besitzen:

kreis = Kreis(5)
rechteck = Rechteck(4, 6)

print(f"Fläche des Kreises: {kreis.berechne_Fläche()}")
print(f"Fläche des Rechtecks: {rechteck.berechne_Fläche()}")

Der Versuch, etwa mit form = Form() eine Instanz der abstrakten Klasse zu erzeugen, führt dagegen zu einem Fehler. Das ist richtig und gut, denn sonst könnte man form.berechne_Fläche() aufrufen, aber es ist nicht festgelegt, was diese Methode in der allgemeinen Form machen soll.

Wenn man in einer Kindklasse einer abstrakten Elternklasse nicht alle deren abstrakten Methoden implementiert, bleibt diese Kindklasse abstrakt. Auch von ihr lassen sich dann keine Instanzen erzeugen.

Warum sind abstrakte Klassen nützlich?

Sicherstellen der Methodendefinition: Abstrakte Klassen sorgen dafür, dass jede abgeleitete Klasse bestimmte Methoden implementiert. Das führt zu einem einheitlicheren und vorhersehbaren Verhalten.

Einheitliche Struktur: Abstrakte Klassen schaffen eine einheitliche Struktur, wodurch verschiedene Klassen auf gleiche Weise benutzt werden können.

Bessere Übersicht: Abstrakte Klassen helfen dabei, den Code besser zu organisieren und die Beziehungen zwischen verschiedenen Klassen klarer zu machen.

Collections

In Python gibt es mehrere Arten von Datenstrukturen, die als Collections bezeichnet werden. Diese Collections bieten verschiedene Möglichkeiten, um Daten zu speichern und zu verwalten. Zu den wichtigsten Collections gehören Liste (die schon bekannt sein sollte), Tupel (Tuple), Menge (Set) und Wörterbuch (Dictionary; im Deutschen sagt man seltenst Wörterbuch, sondern allenfalls assoziatives Array, was auch nicht wirklich Deutsch ist).

Tupel (Tuple)

Ein Tupel ist ähnlich wie eine Liste, aber es ist unveränderlich, das heißt, einmal erzeugt, kann es nicht mehr geändert werden. Tupel werden mit runden Klammern (...) erstellt:

# Ein einfaches Tupel
mein_tupel = (1, 2, 3)

# Ein Tupel mit verschiedenen Datentypen
gemischtes_tupel = (1, "Hallo", 3.14)

# Oder mit Typenangabe
gemischtes_tupel2: tuple[int, str, float] = (1, "Hallo", 3.14)

Der Zugriff auf die Elemente erfolgt mit eckigen Klammern, wie bei Listen:

print(mein_tupel[0])  # Ausgabe: 1
print(gemischtes_tupel[1])  # Ausgabe: Hallo

Man kann allerdings nicht die Elemente ersetzen oder die Länge ändern:

gemischtes_tupel[0] = 23  # Fehler!
gemischtes_tupel.append("bla")  # Fehler!

Warum Tupel verwenden?

Menge (Set)

Eine Menge ist eine Sammlung von Elementen, die es alle höchstens einmal darin gibt. Die Elemente sind nicht auf garantierte Art geordnet, haben also insbesondere keine Indizes [0], [1] usw., anders als die Elemente einer Liste oder eines Tupels. Mengen werden mit geschweiften Klammern {...} erstellt, die leere Menge mittels set(), also nicht wie in der Mathematik als {}.

# Eine Menge mit ein vier Elementen
mein_set = {23, 42, 'bla', 7}

# Oder mit Typenangabe
mein_set2: set[int | str] = {23, 42, 'bla', 7}

# Die leere Menge
leeres_set = set()

Elemente werden mit add und remove hinzugefügt bzw. entfernt. Mit in und not in prüft man, ob ein gegebenes Objekt enthalten ist:

mein_set.add('blubb')  # Hinzufügen eines Elements
mein_set.remove(42)  # Entfernen eines Elements
print(mein_set)  # Ausgabe: {'blubb', 23, 'bla', 7} in irgendeiner Reihenfolge
print('abc' not in mein_set)  # Ausgabe: True

Python beherrscht viele Operationen der Mengenlehre. Unter anderem lassen sich Mengen mit | vereinigen, mit & schneiden, mit - voneinander abziehen:

set1 = {1, 2, 3}
set2 = {3, 4, 5}
set3 = {5, 6, 7}
print(set1 & set2 | set3)  # Ausgabe: {3, 5, 6, 7} in irgendeiner Reihenfolge

Warum Mengen verwenden?

Dictionary

Ein Dictionary speichert Paare von Schlüssel (Key) und Wert (Value), so wie ein Englisch-Deutsch-Wörterbuch zu jedem Schlüssel (das jeweilige englische Wort) einen Wert (die jeweilige deutsche Übersetzung) enthält. Unter dem Schlüssel schlägt man nach und liest damit den Wert aus oder ändert ihn.

Dictionaries sind geordnet (seit Python 3.7) und ihre Elemente sind indiziert. Dictionaries werden mit geschweiften Klammern {...} erstellt, was zu Verwechselungen mit Sets führen kann. Aber in den Schweifklammern von Dictionaries stehen nicht einzelne Werte, sondern Paare der Art Schüssel: Wert. Mit den leeren Schweifklammern {} erzeugt man ein leeres Dictionary (weshalb die leere Menge stattdessen mit set() erzeugt wird).

Der Typ für das folgende Dictionary ist dict[str, str | int]:

# Ein Wörterbuch mit drei Schlüssel-Wert-Paaren
mein_Dict = {
    'Name': 'Alice',
    'Alter': 25,
    'Stadt': 'Berlin'
}

Wie man mit Hilfe von Zahlen-Indizes auf die Elemente von Listen und Tupeln zugreift, so greift man mit den Schlüsseln auf die Werte des Dictionary zu:

print(mein_Dict['Name'])  # Ausgabe: Alice
mein_Dict['Alter'] = 26

Auf diese Art fügt man Schlüssel-Wert-Paare hinzu oder entfernt sie:

mein_Dict['Beruf'] = 'Entwicklerin'  # Hinzufügen eines neuen Paares
del mein_Dict['Stadt']  # Entfernen eines Paares
print(mein_Dict)  # Ausgabe: {'Name': 'Alice', 'Alter': 25, 'Beruf': 'Entwicklerin'}

Oft benutzt man Listen von Dictionaries (langsam nochmal lesen: Listen! von! Dictionaries!) als Datenspeicher:

personen: list[dict[str, str | int]] = [
    {'Name': 'Alice', 'Alter': 25},
    {'Name': 'Bob', 'Alter': 30},
    {'Name': 'Charlie', 'Alter': 35},
    {'Name': 'Diana', 'Alter': 40}
]

gesamtes_alter = sum(person['Alter'] for person in personen)  # Das "for ... in ..." hier ist eine "Generator Expression", Erklärung siehe unten.

durchschnittsalter = gesamtes_alter / len(personen)

print(f'Das Durchschnittsalter beträgt: {durchschnittsalter:.1f}')

Warum Dictionaries verwenden?

Spezialfunktionen der Collections

Einige der Collections beherrschen Spezialfunktionen wie Sortierung, Unpacking, Comprehension, Generator Expression. Falls eine gegebene Art Collection die jeweilige Funktion nicht unterstützt, verwandelt man diese Collection einfach zunächst in eine Liste.

Sortierung

Listen lassen sich sortieren. Dies sind die einfachsten Varianten, noch ohne Angabe, auf welche spezielle Art sortiert werden soll:

meine_Liste = [3, 1, 4, 1, 5]
meine_Liste.sort()  # In-place sortieren
print(meine_Liste)  # Ausgabe: [1, 1, 3, 4, 5]

sortierte_Liste = sorted(meine_Liste)  # Gibt eine neue sortierte Liste zurück
print(sortierte_Liste)  # Ausgabe: [1, 1, 3, 4, 5]

Unpacking

Listen, Tupel sowie Objekte geeignet gebauter (Iterable) Klassen lassen sich direkt in Einzelvariablen zerlegen:

meine_Liste = [1, 2, 3]
a, b, c = meine_Liste  # Unpacking
print(a, b, c)  # Ausgabe: 1 2 3

Unpacking benutzt man oft, wenn eine Funktion ein zusammengesetztes Ergebnis liefert. So kam in der Einheit zu Datenvisualisierung schon die Funktion scipy.stats.linregress zur Bestimmung der Regressionsgeraden vor:

steigung, achsenabschnitt, _, _, _ = stats.linregress(x, y)

Die Unterstriche besagen hier, dass man die betreffenden Daten keiner Variablen zuweisen will.

Comprehension

Listen lassen sich per List Comprehension in neue Listen verwandeln:

meine_Liste = [1, 2, 3, 4, 5]
quadrate = [x ** 2 for x in meine_Liste]
print(quadrate)  # Ausgabe: [1, 4, 9, 16, 25]

Man kann obendrein eine Bedingung angeben, welche Elemente benutzt werden sollen:

meine_Liste = [1, 2, 3, 4, 5]
quadrate = [x ** 2 for x in meine_Liste if x > 3]
print(quadrate)  # Ausgabe: [16, 25]

Mengen lassen sich per Set Comprehension erzeugen:

meine_Liste = [-2, -1, 0, 1, 2]
quadrate = {x ** 2 for x in meine_Liste if x != 0}
print(quadrate)  # Ausgabe: {1, 4}

Mit Dictionaries klappt das ähnlich; bei der Dictionary Comprehension gibt es aber Schlüssel (meist k für key) und Werte (meist v für value):

mein_Dict = {'a': 1, 'b': 2, 'c': 3}
quadrate = {k + k: v ** 2 for k, v in mein_Dict.items() if v > 1}
print(quadrate)  # Ausgabe: {'bb': 4, 'cc': 9}

Generator Expression

Eine Generator Expression ähnelt der List Comprehension, allerdings rechnet sie nicht alle Werte vorher aus und speichert die, sondern liefert auf Anfrage einen Wert nach dem anderen. Generator Expressions können bei großen Datensätzen extrem Speicherbedarf sparen und, wenn man vor dem Ende abbricht, auch Rechenzeit sparen. Umgekehrt kosten Generator Expressions mehr Rechenzeit als Listen, wenn die Ergebnisse im Programm mehrfach benötigt werden, denn mit Generator Expressions müssten sie dann auch mehrfach berechnet werden.

Generator Expressions stehen in runden statt eckigen Klammern; in einigen Situationen können die runden Klammern sogar weggelassen werden.

meine_Liste = [1, 2, 3, 4, 5, 6]

# Hier entsteht mit [...] erst die komplette Liste
for i in [x ** 2 for x in meine_Liste if x > 3]:
    print(i)  # Ausgabe: 16, 25, 36

# Hier wird (...) immer wieder nach dem nächsten Element gefragt
for i in (x ** 2 for x in meine_Liste if x > 3):
    print(i)  # Ausgabe: 16, 25, 36

Das schon aus den Schleifen bekannte range verhält sich ähnlich wie eine solche Generator Expression.