Home | Lehre | Videos | Texte | Vorträge | Software | Person | Impressum, Datenschutzerklärung |
Stand: 2024-06-11
weitgehend formuliert von ChatGPT-4o
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.
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(5)
kreis = Rechteck(4, 6)
rechteck
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.
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.
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).
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
= (1, 2, 3)
mein_tupel
# Ein Tupel mit verschiedenen Datentypen
= (1, "Hallo", 3.14)
gemischtes_tupel
# Oder mit Typenangabe
tuple[int, str, float] = (1, "Hallo", 3.14) gemischtes_tupel2:
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:
0] = 23 # Fehler!
gemischtes_tupel["bla") # Fehler! gemischtes_tupel.append(
Warum Tupel verwenden?
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
= {23, 42, 'bla', 7}
mein_set
# Oder mit Typenangabe
set[int | str] = {23, 42, 'bla', 7}
mein_set2:
# Die leere Menge
= set() leeres_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:
'blubb') # Hinzufügen eines Elements
mein_set.add(42) # Entfernen eines Elements
mein_set.remove(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:
= {1, 2, 3}
set1 = {3, 4, 5}
set2 = {5, 6, 7}
set3 print(set1 & set2 | set3) # Ausgabe: {3, 5, 6, 7} in irgendeiner Reihenfolge
Warum Mengen verwenden?
mit Milch,
mit Zucker,
mit Plätzchen.
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
'Alter'] = 26 mein_Dict[
Auf diese Art fügt man Schlüssel-Wert-Paare hinzu oder entfernt sie:
'Beruf'] = 'Entwicklerin' # Hinzufügen eines neuen Paares
mein_Dict[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:
list[dict[str, str | int]] = [
personen: 'Name': 'Alice', 'Alter': 25},
{'Name': 'Bob', 'Alter': 30},
{'Name': 'Charlie', 'Alter': 35},
{'Name': 'Diana', 'Alter': 40}
{
]
= sum(person['Alter'] for person in personen) # Das "for ... in ..." hier ist eine "Generator Expression", Erklärung siehe unten.
gesamtes_alter
= gesamtes_alter / len(personen)
durchschnittsalter
print(f'Das Durchschnittsalter beträgt: {durchschnittsalter:.1f}')
Warum Dictionaries verwenden?
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.
Listen lassen sich sortieren. Dies sind die einfachsten Varianten, noch ohne Angabe, auf welche spezielle Art sortiert werden soll:
= [3, 1, 4, 1, 5]
meine_Liste # In-place sortieren
meine_Liste.sort() print(meine_Liste) # Ausgabe: [1, 1, 3, 4, 5]
= sorted(meine_Liste) # Gibt eine neue sortierte Liste zurück
sortierte_Liste print(sortierte_Liste) # Ausgabe: [1, 1, 3, 4, 5]
Listen, Tupel sowie Objekte geeignet gebauter (Iterable
)
Klassen lassen sich direkt in Einzelvariablen zerlegen:
= [1, 2, 3]
meine_Liste = meine_Liste # Unpacking
a, b, c 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:
= stats.linregress(x, y) steigung, achsenabschnitt, _, _, _
Die Unterstriche besagen hier, dass man die betreffenden Daten keiner Variablen zuweisen will.
Listen lassen sich per List Comprehension
in neue Listen
verwandeln:
= [1, 2, 3, 4, 5]
meine_Liste = [x ** 2 for x in meine_Liste]
quadrate print(quadrate) # Ausgabe: [1, 4, 9, 16, 25]
Man kann obendrein eine Bedingung angeben, welche Elemente benutzt werden sollen:
= [1, 2, 3, 4, 5]
meine_Liste = [x ** 2 for x in meine_Liste if x > 3]
quadrate print(quadrate) # Ausgabe: [16, 25]
Mengen lassen sich per Set Comprehension
erzeugen:
= [-2, -1, 0, 1, 2]
meine_Liste = {x ** 2 for x in meine_Liste if x != 0}
quadrate 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
):
= {'a': 1, 'b': 2, 'c': 3}
mein_Dict = {k + k: v ** 2 for k, v in mein_Dict.items() if v > 1}
quadrate print(quadrate) # Ausgabe: {'bb': 4, 'cc': 9}
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.
= [1, 2, 3, 4, 5, 6]
meine_Liste
# 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.