Home | Lehre | Videos | Texte | Vorträge | Software | Person | Impressum, Datenschutzerklärung |
Stand: 2024-05-27
weitgehend formuliert von ChatGPT 4o
Vererbung ist ein wichtiges Konzept in der objektorientierten Programmierung. Es ermöglicht, eine neue Klasse (Kindklasse) zu erstellen, die die Eigenschaften und Methoden einer bestehenden Klasse (Elternklasse) übernimmt. Dies kann helfen, Code wiederzuverwenden und die Struktur von Programmen klarer und wartbarer zu gestalten.
Angenommen, wir möchten ein Bestellsystem für einen Online-Shop
entwickeln. Wir haben eine allgemeine Klasse Produkt
, und
daraus abgeleitet spezifische Klassen für verschiedene Produkttypen wie
Buch
und Elektronik
.
Produkt
class Produkt:
def __init__(self, name, preis):
self.name = name
self.preis = preis
def zeige_info(self):
print(f'Produkt: {self.name}, Preis: {self.preis:.2f} EUR')
Produkt
-Klasse hat einen Konstruktor
(__init__
), der die Attribute name
und
preis
initialisiert.zeige_info
gibt die grundlegenden
Informationen des Produkts aus.class Buch(Produkt):
def __init__(self, name, preis, autor):
super().__init__(name, preis)
self.autor = autor
def zeige_info(self):
super().zeige_info()
print(f'Autor: {self.autor}')
class Elektronik(Produkt):
def __init__(self, name, preis, garantie):
super().__init__(name, preis)
self.garantie = garantie
def zeige_info(self):
super().zeige_info()
print(f'Garantie: {self.garantie} Jahre')
Buch
-Klasse erbt von Produkt
und fügt
ein neues Attribut autor
hinzu.Elektronik
-Klasse erbt ebenfalls von
Produkt
und fügt ein neues Attribut garantie
hinzu.zeige_info
-Methode, um zusätzliche Informationen
anzuzeigen.super()
wird benutzt, um auf Methoden der Elternklasse =
Oberklasse = superclass zuzugreifen, die in der Kindklasse überschrieben
sind. Warum würde im Beispiel oben self.zeige_info()
statt super().zeige_info()
Unsinn sein? Vorsicht:
self
ist eine Variable, aber super()
ein
Funktionsaufruf, also mit Klammern dahinter.
# Erstellen von Produktinstanzen
= Produkt('Allgemeines Produkt', 19.99)
produkt1 = Buch('Der Python-Kurs', 29.99, 'John Doe')
buch1 = Elektronik('Smartphone', 399.99, 2)
elektronik1
# Informationen anzeigen
produkt1.zeige_info()# Ausgabe: Produkt: Allgemeines Produkt, Preis: 19.99 EUR
buch1.zeige_info()# Ausgabe:
# Produkt: Der Python-Kurs, Preis: 29.99 EUR
# Autor: John Doe
elektronik1.zeige_info()# Ausgabe:
# Produkt: Smartphone, Preis: 399.99 EUR
# Garantie: 2 Jahre
Durch die Verwendung von Vererbung können wir die allgemeine
Funktionalität in der Produkt
-Klasse definieren und dann
spezialisierte Klassen wie Buch
und Elektronik
erstellen, die diese Funktionalität erweitern oder anpassen. Dies führt
zu einem klaren und übersichtlichen Code, der leicht zu erweitern und zu
warten ist.
Produkt
ist die Oberklasse = Basisklasse = Elternklasse
der Klassen Buch
und Elektronik
. Diese
letzteren beiden Klassen sind Unterklassen = Ableitungen = Kindklassen
von Produkt
.
Das Spiel lässt sich weitertreiben, indem etwa eine Klasse
Notebook
von der Klasse Elektronik
erbt.
Notebook
wäre dann eine Kindklasse einer Kindklasse von
Produkt
. (Nein, man sagt nicht Enkelklasse
dazu.)
Das Überschreiben (engl. overriding
) von Methoden bedeutet,
dass eine Methode in einer abgeleiteten Klasse eine Methode der
Basisklasse mit demselben Namen ersetzt. Dadurch kann das Verhalten der
Methode in der abgeleiteten Klasse spezifiziert oder angepasst
werden.
Das Überschreiben von Methoden ist aus mehreren Gründen nützlich:
Spezialisierung des Verhaltens: Es ermöglicht einer abgeleiteten Klasse, das Verhalten der geerbten Methoden zu spezifizieren und zu ändern.
Polymorphismus: Es unterstützt das Konzept des Polymorphismus, bei dem eine Methode unterschiedlich implementiert wird, basierend darauf, welches Objekt sie aufruft. (Nachher mehr dazu.)
Wiederverwendbarkeit und Wartbarkeit: Durch das Überschreiben können wir Code wiederverwenden und dennoch flexible und erweiterbare Entwürfe erstellen, ohne die Basisklasse ändern zu müssen.
Um keine große Verwirrung zu erzeugen, sollte die überschriebende Methode dieselben Typen an Parametern nehmen wie die Oberklasse. (In C++, Java usw. ist das Pflicht für das Überschreiben.)
Ein Vertipper beim Namen der Methode führt dazu, dass man in der
Kindklasse nicht die Methode der Elternklasse überschreibt, sondern eine
neue Methode anlegt, mit fast, aber eben auch nur fast, gleichem Namen.
Um diesen beliebten Fehler automatisch entdecken zu lassen, kann man ab
Python 3.12 in die Zeile vor die Definition der überschreibenden Methode
in der Kindklasse ein @override
schreiben. Dazu ist derzeit
noch vorher ein from typing import override
nötig.
Hier ist das Beispiel von oben noch einmal, aber mit einem Fokus auf
das Überschreiben der zeige_info
-Methode.
Die Produkt
-Klasse definiert eine allgemeine Methode
zeige_info
, die grundlegende Informationen über das Produkt
anzeigt:
class Produkt:
def __init__(self, name, preis):
self.name = name
self.preis = preis
def zeige_info(self):
print(f'Produkt: {self.name}, Preis: {self.preis:.2f} EUR')
Die Buch
-Klasse erbt von Produkt
und fügt
das Attribut autor
hinzu. Die Klasse Buch
überschreibt die Methode zeige_info
, um die spezifischen
Informationen des Buches anzuzeigen. super().zeige_info()
ruft die zeige_info
-Methode der Basisklasse auf, um
zunächst die allgemeinen Produktinformationen anzuzeigen:
class Buch(Produkt):
def __init__(self, name, preis, autor):
super().__init__(name, preis)
self.autor = autor
def zeige_info(self):
super().zeige_info()
print(f'Autor: {self.autor}')
Entsprechendes passiert in der Methode zeige_info
der
Elektronik
-Klasse:
class Elektronik(Produkt):
def __init__(self, name, preis, garantie):
super().__init__(name, preis)
self.garantie = garantie
def zeige_info(self):
super().zeige_info()
print(f'Garantie: {self.garantie} Jahre')
Durch das Überschreiben der zeige_info
-Methode in den
abgeleiteten Klassen können wir spezifische Informationen für
unterschiedliche Produkttypen anzeigen, ohne die allgemeine Logik der
Basisklasse Produkt
zu verändern. Dies zeigt, wie Vererbung
und Überschreiben zusammenarbeiten, um einen flexiblen und erweiterbaren
Code zu erstellen.
Polymorphie ist ein fundamentales Konzept in der objektorientierten Programmierung, das es Objekten verschiedener Klassen ermöglicht, auf die gleiche Art angesprochen zu werden. Eine der Hauptideen hinter Polymorphie ist, dass Objekte einer Kindklasse (und Kindeskindklasse usw.) überall dort verwendet werden können, wo Objekte der Elternklasse erwartet werden. Das heißt, Kindklassen (Unterklassen) können die Elternklassen (Oberklassen) vertreten. Die Kindklassen können dabei aber zusätzliche Attribute und Methoden haben und sie können Methoden ihrer Elternklasse überschreiben und damit den Effekt dieser Methoden ändern.
Der Online-Shop aus dem bereits verwendeten Beispiel benötigt einen
Datenspeicher für den Gesamtbestand an Produkten. Hierbei verwenden wir
eine Liste, die Objekte verschiedener Produktklassen enthält, aber
dennoch als Liste von Produkt
-Objekten betrachtet wird.
Dank Polymorphie können wir eine Liste von Produkten definieren, die
sowohl Produkt
, Buch
als auch
Elektronik
-Objekte enthält, denn Buch
und
Elektronik
können ihre Oberklasse Produkt
vertreten. Die Methode zeige_info
wird korrekt für jeden
Produkttyp aufgerufen.
# Liste von verschiedenen Produktobjekten
list[Produkt] = [
produkte: 'Allgemeines Produkt', 19.99),
Produkt('Der Python-Kurs', 29.99, 'John Doe'),
Buch('Smartphone', 399.99, 2)
Elektronik(
]
# Informationen anzeigen für alle Produkte in der Liste
for produkt in produkte:
produkt.zeige_info()print() # Leere Zeile zur besseren Lesbarkeit
Flexibilität: Die Methode
zeige_info
kann für verschiedene Produkttypen verwendet
werden, ohne dass die spezifischen Typen bekannt sein müssen.
Erweiterbarkeit: Neue Produkttypen können leicht
hinzugefügt werden, indem einfach neue Klassen erstellt werden, die von
Produkt
erben.
Klarheit und Wartbarkeit: Der Code ist klarer und modularer, da er allgemeine Operationen abstrahiert und spezialisierte Implementierungen verbirgt.
Durch Polymorphie können wir eine einheitliche Schnittstelle (Sammlung der Methoden) für verschiedene Produkttypen bereitstellen, was zu einem flexiblen, erweiterbaren und gut strukturierten Code führt.
Sprachen wie C++ und Java sind sehr strikt darin, festzulegen, welche Klassen auf welche Bestandteile welcher anderen Klassen zugreifen können. In Python ist das lockerer. Unterstriche vor den Namen von Attributen und Methoden spielen dabei die Hauptrolle.
_
)Ein einzelner Unterstrich vor einem Attribut oder einer Methode ist
eine Konvention, die anzeigt, dass das Element als geschützt betrachtet
werden soll, sozusagen unter der Motorhaube liegend oder versehen mit
der Aufschrift Finger weg!
. Es soll (soll!) nicht direkt von
außerhalb der Klasse verwendet werden. Das ist wohlgemerkt nur ein
soll nicht
, aber kein darf nicht
.
Der Gedanke hinter diesem (zugegebenermaßen schwachen) Schutz ist, dass man dann unter der Motorhaube der Klasse Änderungen vornehmen kann, ohne auf andere Klassen Rücksicht nehmen zu müssen. Das gelingt um so besser, je mehr Attribute und Methoden man mit dem Unterstrich als geschützt markiert und je mehr sich andere Klassen an diesen Hinweis halten, also diese Attribute und Methoden nicht direkt ansprechen.
Ein Beispiel:
class Beispiel:
def __init__(self):
self._geschuetztes_attribut = 42
def _geschuetzte_methode(self):
print('Diese Methode ist geschützt.')
= Beispiel()
obj print(obj._geschuetztes_attribut) # 42
# Diese Methode ist geschützt. obj._geschuetzte_methode()
In diesem Beispiel werden _geschuetztes_attribut
und
_geschuetzte_methode
mit einem einfachen Unterstrich
versehen, um anzuzeigen, dass sie nicht außerhalb der Klasse verwendet
werden sollten.
__
)Doppelte Unterstriche vor einem Attribut oder einer Methode sind erstens ein (minimal) stärkerer Schutz und ersparen zweitens Stress beim Ableiten von Klassen. Doppelte Unterstriche führen anders als der einfache Unterstrich zu einer Aktion der Python-Laufzeitumgebung: Intern wird dem Namen des Attributs oder der Methode der Name der Klasse vorangestellt, mit einem führenden Unterstrich.
Dies ist keine ernstzunehmende Schutzmaßnahme, um das Attribut oder die Methode zu verbergen, aber es erleichtert das Ableiten: Eine Unterklasse kann die gleichen Namen für eigene weitere Attribute und Methoden verwenden wie ihre Oberklasse; sie werden dann zusätzlich angelegt, weil sie ja intern anders heißen. So verursachen sie keine Konflikte.
Doppelte Unterstriche sind vor allem in tiefen Klassenhierarchien mit Kindeskindeskinderklassen usw. hilfreich, in denen man den Überblick darüber verliert, welche Attribute und Methoden die Oberklassen und Oberoberklassen usw. haben, oder in denen die Gefahr besteht, dass später in einer Oberklasse ein Attribut oder eine Methode ergänzt wird, dessen/deren Namen es schon in einer der Unterklassen gibt.
Ein Beispiel:
class Beispiel:
def __init__(self):
self.__privates_attribut = 42
def __private_methode(self):
print('Diese Methode ist privat.')
def zugriff_methode(self):
self.__private_methode()
= Beispiel()
obj # Direkter Zugriff schlägt fehl:
# print(obj.__privates_attribut) # AttributError
# obj.__private_methode() # AttributError
# Zugriff über Namensmangling:
print(obj._Beispiel__privates_attribut) # 42
# Diese Methode ist privat. obj._Beispiel__private_methode()
Alle bisher erwähnten Attribute und Methoden haben sich jeweils auf
eine Instanz, also auf ein Objekt der Klasse bezogen: Jede Instanz der
Klasse besitzt eigene Werte für die Attribute und hat in den Methoden
die Variable self
, die auf die aktuelle Instanz verweist.
In der Objektorientierung unterscheidet man zwischen solchen
Instanz-Attributen und -Methoden, die spezifisch für eine
Instanz (ein Objekt) einer Klasse sind, und Klassen-Attributen
und -Methoden, die für die gesamte Klasse und alle ihre Instanzen
gelten. Letztere heißen oft auch statisch.
In Python gibt es (anderes als in den meisten anderen Sprachen) eine Unterscheidung zwischen Klassenmethoden und statischen Methoden. Erstere können einfach auf Klassenattribute zugreifen, letztere nicht.
In Python wird ein Klassen-Attribut in der Klassendefinition außerhalb aller Methoden definiert:
class Fahrzeug:
# Klassen-Attribut
= 0
anzahl_fahrzeuge
def __init__(self, marke, modell):
# Instanz-Attribute
self.marke = marke
self.modell = modell
# Erhöhen der Anzahl der Fahrzeuge bei jeder Instanziierung
+= 1
Fahrzeug.anzahl_fahrzeuge
# Instanziiere Fahrzeuge
= Fahrzeug('Toyota', 'Corolla')
auto1 = Fahrzeug('Honda', 'Civic')
auto2
print(f'Anzahl der Fahrzeuge: {Fahrzeug.anzahl_fahrzeuge}') # Ausgabe: 2
Eine Klassen-Methode wird mit dem Dekorator @classmethod
und einem speziellen ersten Parameter cls
(der die Klasse
selbst repräsentiert) definiert:
class Fahrzeug:
= 0
anzahl_fahrzeuge
def __init__(self, marke, modell):
self.marke = marke
self.modell = modell
+= 1
Fahrzeug.anzahl_fahrzeuge
@classmethod
def get_anzahl_fahrzeuge(cls):
return cls.anzahl_fahrzeuge
# Instanziiere Fahrzeuge
= Fahrzeug('Toyota', 'Corolla')
auto1 = Fahrzeug('Honda', 'Civic')
auto2
print(f'Anzahl der Fahrzeuge: {Fahrzeug.get_anzahl_fahrzeuge()}') # Ausgabe: 2
In einer Klassenmethode hat also cls
die Rolle des
self
in einer Instanzmethode. Eine Instanzmethode wird
mittels einer konkreten Instanz aufgerufen wird, nach dem Muster
auto1.tu_etwas()
. Eine Klassenmethode wird dagegen über die
Klasse selbst aufgerufen: Fahrzeug.tu_etwas()
.
Anders als eine Klassenmethode arbeitet eine statische Methode
unabhängig von der Klasse und ihren Instanzen, hat also keinen Zugriff
auf cls
oder self
. Sie wird mit dem Dekorator
@staticmethod
definiert und ebenfalls über die Klasse
selbst aufgerufen, nicht über eine Instanz:
class Fahrzeug:
= 0
anzahl_fahrzeuge
def __init__(self, marke, modell):
self.marke = marke
self.modell = modell
+= 1
Fahrzeug.anzahl_fahrzeuge
@classmethod
def get_anzahl_fahrzeuge(cls):
return cls.anzahl_fahrzeuge
@staticmethod
def beispiel_static_method():
print('Dies ist eine statische Methode.')
# Aufruf der statischen Methode
# Ausgabe: Dies ist eine statische Methode. Fahrzeug.beispiel_static_method()
In Python enthalten alle (alle!) Variablen Referenzen und nicht direkt Werte. Das ist anders als in C, C++, Java und vielen anderen Sprachen, in denen insbesondere Zahlenvariablen direkt Werte speichern, statt (etwa als Zeiger) auf Werte zu verweisen.
Dies bedeutet, dass Variablen in Python auf Speicherorte verweisen, an denen Daten gespeichert sind, anstatt die tatsächlichen Daten selbst zu speichern. Python arbeitet also so, als ob man in C ausschließlich Zeiger (Pointer) nutzen würde. Das kann für Überraschungen sorgen, weil man dasselbe Objekt über verschiedene Referenzen ansprechen und ändern kann.
Mehrere Variablen können auf dasselbe Objekte verweisen. Dies bedeutet, dass, wenn Sie ein Objekt über dieser Variable ändern, diese Änderung ebenfalls über die anderen Variablen sichtbar ist:
= [1, 2, 3]
liste1 = liste1 # Es gibt weiterhin nur eine Liste!
liste2
4)
liste2.append(
print(liste1) # Ausgabe: [1, 2, 3, 4]
print(liste2) # Ausgabe: [1, 2, 3, 4]
In diesem Beispiel verweisen sowohl liste1
als auch
liste2
auf dasselbe Listenobjekt. Wenn Sie
liste2
ändern, wird liste1
ebenfalls geändert,
da beide Variablen auf dasselbe Objekt verweisen.
Wenn zwei Referenzen auf dieselbe Instanz einer Klasse zeigen, können Änderungen über eine Referenz unerwartete Auswirkungen darauf haben, was man unter der anderen Referenz sieht. Dies kann besonders verwirrend sein, wenn man nicht bewusst ist, dass beide Referenzen tatsächlich auf dasselbe Objekt im Speicher zeigen.
Stellen wir uns ein Szenario vor, bei dem wir eine Klasse
Konto
haben, die ein Bankkonto darstellt. Wir erstellen ein
Kontoobjekt und verwenden zwei verschiedene Referenzen darauf.
class Konto:
def __init__(self, inhaber: str, kontostand: float):
self.inhaber = inhaber
self.kontostand = kontostand
def einzahlen(self, betrag: float):
self.kontostand += betrag
def abheben(self, betrag: float):
if betrag <= self.kontostand:
self.kontostand -= betrag
else:
print('Nicht genügend Guthaben.')
def zeige_info(self):
print(f'Inhaber: {self.inhaber}, Kontostand: {self.kontostand:.2f} EUR')
# Erstellen eines Konto-Objekts
= Konto('Alice', 1000.00)
konto1 # Erstellen einer zweiten Referenz auf dasselbe Konto-Objekt
= konto1
konto2
# Einzahlen über die erste Referenz
500.00)
konto1.einzahlen(
# Anzeigen des Kontostands über beide Referenzen
# Ausgabe: Inhaber: Alice, Kontostand: 1500.00 EUR
konto1.zeige_info() # Ausgabe: Inhaber: Alice, Kontostand: 1500.00 EUR
konto2.zeige_info()
# Abheben über die zweite Referenz
200.00)
konto2.abheben(
# Anzeigen des Kontostands über beide Referenzen
# Ausgabe: Inhaber: Alice, Kontostand: 1300.00 EUR
konto1.zeige_info() # Ausgabe: Inhaber: Alice, Kontostand: 1300.00 EUR konto2.zeige_info()
Hier sind einige Szenarien, in denen diese Art von Verwirrung auftreten kann:
Unbeabsichtigte Nebenwirkungen: Änderungen, die über eine Referenz vorgenommen werden, beeinflussen auch die andere Referenz. Wenn man dies nicht erwartet, kann dies zu schwer nachvollziehbaren Fehlern führen.
Fehlersuche und Debugging: Es kann schwierig sein, den Ursprung von Änderungen zu identifizieren, da mehrere Referenzen auf dasselbe Objekt zugreifen und es ändern können.
Komplexe Datenstrukturen: In komplexeren Programmen mit verschachtelten Datenstrukturen kann das Verfolgen von Referenzen noch schwieriger sein.
Unveränderbare (immutable
) Python-Objekte wie Zahlen und
Strings können nicht direkt geändert werden. Stattdessen erzeugt jede
(scheinbare!) Modifikation eines unveränderbaren Objekts ein neues
Objekt. Dadurch merkt man oft nicht, dass Python mit Referenzen
arbeitet, da Änderungen scheinbar lokal auf die jeweilige Variable
beschränkt bleiben.
= 10
zahl1 = zahl1
zahl2
+= 5
zahl2
print(zahl1) # Ausgabe: 10
print(zahl2) # Ausgabe: 15
Hier verweisen zahl1
und zahl2
zunächst auf
dasselbe Integer-Objekt 10
. Wenn zahl2
jedoch
geändert wird (zahl2 += 5
), erzeugt Python ein neues
Integer-Objekt 15
und zahl2
verweist nun auf
dieses neue Objekt, während zahl1
weiterhin auf
10
verweist.
Strings sind ebenfalls unveränderbar. Jede Modifikation an einem String erzeugt ein neues String-Objekt.
= 'Hallo'
string1 = string1
string2
+= ', Welt!'
string2
print(string1) # Ausgabe: Hallo
print(string2) # Ausgabe: Hallo, Welt!
Hier verweist string1
auf ein String-Objekt mit dem
Inhalt Hallo
und string2
auf dasselbe Objekt. Wenn
string2
geändert wird, erzeugt Python ein neues
String-Objekt Hallo, Welt!
und string2
verweist nun
auf dieses neue Objekt, während string1
weiterhin auf
Hallo
verweist.
Unveränderbare Objekte machen einige Arten an Fehlern unmöglich. Deshalb schreibt man oft auch eigene Klassen so, dass sie nie Veränderungen an einer einmal gebauten Instanz vornehmen.
None
In Python ist None
ein spezieller Wert, der verwendet
wird, um das Fehlen eines konkreten Wertes zu kennzeichnen. (Pythons
None
entspricht in C und C++ der Nullzeiger.) Es ist ein
einzelnes Objekt, das oft verwendet wird, um Variablen zu initialisieren
oder anzuzeigen, dass eine Angabe fehlt oder dass eine Funktion nichts
zurückgibt. Zum Beispiel gibt eine Funktion, die keinen
return
-Befehl enthält, automatisch None
zurück. None
ist auch nützlich, wenn Sie eine Variable
anlegen wollen, die später einen Wert erhalten soll, aber vorerst leer
ist.
0
oder die leere
Zeichenkette ''
. Die Zahl 0 hat den Typ int
und die leere Zeichenkette hat den Typ str
, nicht den Typ
None
. Die Nullreferenz ist vielmehr die Abwesenheit von
konkreten Informationen. 0 Euro zu verdienen, heißt zu wissen, was man
verdient (nämlich nichts). Ein Passwort, das keine Zeichen enthält, ist
ein konkretes (wenn auch schlechtes) Passwort.
Wenn eine Variable wahlweise den Wert None
haben kann,
können Sie dies mit einem Type Hint der Art float | None
darstellen. Wenn eine Funktion nichts zurückgeben soll, kann dies mit
-> None
angegeben werden. Zum Beispiel:
def beispiel_function(wert: float | None) -> None:
if wert is None:
print('Kein Wert angegeben')
else:
print(f'Der Wert ist {wert}')
wert is None
ist
wert is not None
, was für C-Gewöhnte falsch aussieht. Die
Schreibweisen wert == None
bzw. wert != None
funktionieren auch meist, falls wert
nicht eine eigenwillig
geschriebene Klasse hat.
Nullreferenzen wie None
hat deren Erfinder Tony Hoare
später als seinen Milliarden-Dollar-Fehler
bezeichnet, da die Einführung von Nullreferenzen in Programmiersprachen
zu einer Vielzahl von Programmfehlern führte, die in Summe Milliarden
von Dollar gekosten hätten. Nullreferenzen verursachen häufig
Programmabstürze und Sicherheitslücken, weil oft vergessen wird,
ausreichend zu prüfen, ob eine Variable None
ist, bevor man
darauf zugreift. Obwohl Nullreferenzen in vielen Sprachen weit
verbreitet sind, hat die Erkenntnis ihrer Gefahren zu Alternativen
geführt, wie z.B. Option-Typen in Sprachen wie Rust und Kotlin, die
sicherstellen, dass Nullwerte explizit behandelt werden müssen. In
Python tragen gute Praktiken wie Type Hints und sorgfältige Prüfungen
dazu bei, die Risiken zu mindern und robusteren Code zu schreiben.