26. Defensive Programmiertechniken mit Exceptions und Validierung
Defensives Programmieren (defensive programming) bedeutet, Code zu schreiben, der Probleme antizipiert, bevor sie auftreten. Statt anzunehmen, dass alles perfekt funktioniert, validiert defensiver Code Eingaben, behandelt Fehler elegant und überprüft Annahmen. Dieser Ansatz erstellt Programme, die zuverlässiger sind, sich leichter debuggen lassen und weniger wahrscheinlich unerwartet abstürzen.
In früheren Kapiteln haben wir gelernt, wie man Exceptions behandelt, wenn sie auftreten. Jetzt lernen wir, wie man viele Fehler verhindert, bevor sie überhaupt passieren, und wie man Probleme frühzeitig abfängt, wenn sie doch auftreten.
26.1) Funktionsargumente validieren
Funktionen (functions) erhalten oft Daten aus anderen Teilen Ihres Programms oder von Benutzern. Wenn eine Funktion ungültige Daten erhält, kann sie falsche Ergebnisse liefern, mit einem verwirrenden Fehler abstürzen oder anderswo in Ihrem Programm Probleme verursachen. Argumentvalidierung (argument validation) bedeutet, zu prüfen, ob Funktionsargumente Ihre Anforderungen erfüllen, bevor Sie sie verwenden.
26.1.1) Warum Argumente validieren?
Betrachten Sie diese Funktion, die den Prozentanteil der Note eines Schülers berechnet:
def calculate_percentage(points_earned, total_points):
return (points_earned / total_points) * 100
# Verwendung der Funktion
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%") # Output: Grade: 85.0%Das funktioniert mit gültigen Eingaben problemlos. Aber was passiert bei problematischen Daten?
# Problem 1: Division durch Null
percentage = calculate_percentage(85, 0) # ZeroDivisionError!
# Problem 2: Negative Werte (ergibt keinen Sinn)
percentage = calculate_percentage(-10, 100) # -10.0%
# Problem 3: Erreichte Punkte übersteigen Gesamtpunkte (unmöglich)
percentage = calculate_percentage(120, 100) # 120.0%Ohne Validierung stürzt die Funktion entweder ab oder erzeugt unsinnige Ergebnisse. Die Fehlermeldungen erklären nicht, was aus Sicht der Geschäftslogik falsch gelaufen ist – sie zeigen nur technische Fehler an.
26.1.2) Einfache Argumentvalidierung mit Bedingungen
Der einfachste Validierungsansatz verwendet if-Anweisungen, um Argumente zu prüfen und Exceptions auszulösen, wenn sie ungültig sind:
def calculate_percentage(points_earned, total_points):
# total_points validieren
if total_points <= 0:
raise ValueError("total_points must be positive")
# points_earned validieren
if points_earned < 0:
raise ValueError("points_earned cannot be negative")
if points_earned > total_points:
raise ValueError("points_earned cannot exceed total_points")
# Alle Validierungen bestanden – Berechnung ist sicher
return (points_earned / total_points) * 100
# Gültige Verwendung
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%") # Output: Grade: 85.0%
# Ungültige Verwendung – klare Fehlermeldungen
try:
percentage = calculate_percentage(85, 0)
except ValueError as e:
print(f"Error: {e}") # Output: Error: total_points must be positive
try:
percentage = calculate_percentage(-10, 100)
except ValueError as e:
print(f"Error: {e}") # Output: Error: points_earned cannot be negative
try:
percentage = calculate_percentage(120, 100)
except ValueError as e:
print(f"Error: {e}") # Output: Error: points_earned cannot exceed total_pointsJetzt erklärt die Fehlermeldung, wenn etwas schiefgeht, klar, worin das Problem besteht und wie Sie es beheben können.
26.1.3) Argumenttypen validieren
Manchmal müssen Sie sicherstellen, dass Argumente den richtigen Typ haben:
def calculate_discount(price, discount_percent):
# Typen validieren
if not isinstance(price, (int, float)):
raise TypeError("price must be a number")
if not isinstance(discount_percent, (int, float)):
raise TypeError("discount_percent must be a number")
# Werte validieren
if price < 0:
raise ValueError("price cannot be negative")
if not (0 <= discount_percent <= 100):
raise ValueError("discount_percent must be between 0 and 100")
# Rabatt berechnen
discount_amount = price * (discount_percent / 100)
return price - discount_amount
# Gültige Verwendung
final_price = calculate_discount(50.00, 20)
print(f"Final price: ${final_price:.2f}") # Output: Final price: $40.00
# Typfehler
try:
final_price = calculate_discount("50", 20)
except TypeError as e:
print(f"Error: {e}") # Output: Error: price must be a number
# Wertefehler
try:
final_price = calculate_discount(50.00, 150)
except ValueError as e:
print(f"Error: {e}") # Output: Error: discount_percent must be between 0 and 100Die Funktion isinstance() prüft, ob ein Objekt eine Instanz eines angegebenen Typs oder mehrerer Typen ist. Wir übergeben ein Tupel (int, float), um entweder Integers oder Floats zu akzeptieren, da beide gültige numerische Typen für Preise sind.
Wann man Typen validieren sollte: Pythons Philosophie ist „duck typing“ – wenn sich ein Objekt so verhält, wie Sie es brauchen, verwenden Sie es. Typvalidierung ist am nützlichsten, wenn:
- Sie eine Funktion schreiben, die von anderen verwendet wird
- Typfehler später verwirrende Fehler verursachen würden
- Die Funktion Teil einer öffentlichen API oder Bibliothek ist
26.1.4) Collection-Argumente validieren
Wenn Funktionen Listen, Wörterbücher oder andere Sammlungen (collections) akzeptieren, validieren Sie sowohl die Sammlung als auch deren Inhalte:
def calculate_average_grade(grades):
# Die Collection selbst validieren
if not isinstance(grades, list):
raise TypeError("grades must be a list")
if len(grades) == 0:
raise ValueError("grades list cannot be empty")
# Jede Note in der Collection validieren
for i, grade in enumerate(grades):
if not isinstance(grade, (int, float)):
raise TypeError(f"grade at index {i} must be a number, got {type(grade).__name__}")
if not (0 <= grade <= 100):
raise ValueError(f"grade at index {i} must be between 0 and 100, got {grade}")
# Alle Validierungen bestanden
return sum(grades) / len(grades)
# Gültige Verwendung
grades = [85, 92, 78, 95]
average = calculate_average_grade(grades)
print(f"Average: {average:.1f}") # Output: Average: 87.5
# Fehler bei leerer Liste
try:
average = calculate_average_grade([])
except ValueError as e:
print(f"Error: {e}") # Output: Error: grades list cannot be empty
# Ungültiger Notentyp
try:
average = calculate_average_grade([85, "92", 78])
except TypeError as e:
print(f"Error: {e}") # Output: Error: grade at index 1 must be a number, got str
# Ungültiger Notenwert
try:
average = calculate_average_grade([85, 92, 150])
except ValueError as e:
print(f"Error: {e}") # Output: Error: grade at index 2 must be between 0 and 100, got 150Beachten Sie, wie wir beim Validieren von Collection-Elementen den Index in Fehlermeldungen angeben. Das hilft dabei, genau zu identifizieren, welches Element problematisch ist, besonders bei großen Sammlungen.
26.2) Benutzereingaben auf Gültigkeit prüfen
Benutzereingaben sind grundsätzlich unzuverlässig – Benutzer machen Tippfehler, missverstehen Anweisungen oder geben Daten in unerwarteten Formaten ein. Die Validierung von Benutzereingaben verhindert, dass diese Fehler zu Programmabstürzen oder falschen Ergebnissen führen.
26.2.1) Grundlegendes Muster zur Eingabevalidierung
Das grundlegende Muster für Eingabevalidierung kombiniert input() mit Validierungsprüfungen:
# Benutzereingabe holen
age_str = input("Enter your age: ")
# Eingabe validieren
try:
age = int(age_str)
if age < 0:
print("Error: Age cannot be negative")
elif age > 150:
print("Error: Age seems unrealistic")
else:
print(f"You are {age} years old")
except ValueError:
print("Error: Please enter a valid number")Dieses Muster hat drei Teile:
- Die Eingabe als String holen
- Versuchen, sie in den benötigten Typ umzuwandeln
- Prüfen, ob der umgewandelte Wert gültig ist
Schauen wir uns das mit verschiedenen Eingaben in Aktion an:
# Gültige Eingabe
# User enters: 25
# Output: You are 25 years old
# Ungültiger Typ
# User enters: twenty-five
# Output: Error: Please enter a valid number
# Ungültiger Wert (negativ)
# User enters: -5
# Output: Error: Age cannot be negative
# Ungültiger Wert (unrealistisch)
# User enters: 200
# Output: Error: Age seems unrealistic26.2.2) Eingabebereiche und Formate validieren
Einige Eingaben müssen innerhalb bestimmter Bereiche liegen oder bestimmten Formaten entsprechen:
# Einen Monat validieren (1-12)
month_str = input("Enter month (1-12): ")
try:
month = int(month_str)
if not (1 <= month <= 12):
print("Error: Month must be between 1 and 12")
else:
print(f"Month: {month}")
except ValueError:
print("Error: Please enter a whole number")
# E-Mail-Format validieren (einfache Prüfung)
email = input("Enter email: ")
if '@' not in email or '.' not in email:
print("Error: Email must contain @ and .")
else:
print(f"Email: {email}")
# Ja/Nein-Eingabe validieren
response = input("Continue? (yes/no): ").lower().strip()
if response not in ['yes', 'no', 'y', 'n']:
print("Error: Please answer yes or no")
else:
if response in ['yes', 'y']:
print("Continuing...")
else:
print("Stopping...")Die E-Mail-Validierung hier ist absichtlich einfach – sie prüft nur auf eine grundlegende Struktur. Echte E-Mail-Validierung ist deutlich komplexer und verwendet typischerweise reguläre Ausdrücke (die wir in Kapitel 39 kennenlernen).
26.2.3) Hilfreiche Fehlermeldungen bereitstellen
Gute Fehlermeldungen sagen Benutzern genau, was schiefgelaufen ist und wie sie es beheben können:
# Schlechte Fehlermeldung
password = input("Enter password: ")
if len(password) < 8:
print("Error: Invalid password") # Nicht hilfreich!
# Bessere Fehlermeldung
password = input("Enter password: ")
if len(password) < 8:
print("Error: Password must be at least 8 characters long")
print(f"Your password is only {len(password)} characters")
# Noch besser – alle Anforderungen vorab erklären
print("Password requirements:")
print("- At least 8 characters")
print("- Must contain at least one number")
password = input("Enter password: ")
# Länge prüfen
if len(password) < 8:
print(f"Error: Password too short ({len(password)} characters)")
print("Password must be at least 8 characters")
# Auf Ziffer prüfen
elif not any(char.isdigit() for char in password):
print("Error: Password must contain at least one number")
else:
print("Password accepted")Die Funktion any() gibt True zurück, wenn irgendein Element in einem Iterable wahr ist. Hier prüft char.isdigit(), ob jedes Zeichen eine Ziffer ist, und any() sagt uns, ob mindestens ein Zeichen den Test bestanden hat.
26.3) input(), Schleifen und try/except für robuste Eingabebehandlung kombinieren
Einzelne Validierungsprüfungen sind nützlich, aber sie gehen nicht mit wiederholten Benutzerfehlern um. Wenn ein Benutzer ungültige Daten eingibt, sollte Ihr Programm ihm eine weitere Chance geben. Die Kombination von Schleifen mit Validierung erzeugt eine robuste Eingabebehandlung, die so lange nachfragt, bis sie gültige Daten erhält.
26.3.1) Das grundlegende Eingabe-Schleifenmuster
Das grundlegende Muster verwendet eine while-Schleife, die fortgesetzt wird, bis eine gültige Eingabe empfangen wurde:
# So lange fragen, bis wir ein gültiges Alter erhalten
while True:
age_str = input("Enter your age: ")
try:
age = int(age_str)
if age < 0:
print("Error: Age cannot be negative. Please try again.")
elif age > 150:
print("Error: Age seems unrealistic. Please try again.")
else:
# Gültige Eingabe – Schleife verlassen
break
except ValueError:
print("Error: Please enter a valid number.")
print(f"You are {age} years old")Dieses Muster hat mehrere Schlüsselelemente:
while True:erzeugt eine Endlosschleife- Die Validierung passiert innerhalb der Schleife
breakbeendet die Schleife, wenn die Eingabe gültig ist- Fehlermeldungen ermutigen den Benutzer, es erneut zu versuchen
Schauen wir, wie das verschiedene Eingaben behandelt:
# Beispielinteraktion:
# Enter your age: twenty
# Error: Please enter a valid number.
# Enter your age: -5
# Error: Age cannot be negative. Please try again.
# Enter your age: 25
# You are 25 years old26.3.2) Wiederverwendbare Eingabefunktionen erstellen
Wenn Sie dieselbe Art von validierter Eingabe an mehreren Stellen benötigen, erstellen Sie eine Funktion:
def get_positive_integer(prompt):
"""Keep asking until user enters a positive integer."""
while True:
try:
value = int(input(prompt))
if value <= 0:
print("Error: Please enter a positive number.")
else:
return value
except ValueError:
print("Error: Please enter a valid whole number.")
def get_number_in_range(prompt, min_value, max_value):
"""Keep asking until user enters a number in the specified range."""
while True:
try:
value = float(input(prompt))
if value < min_value or value > max_value:
print(f"Error: Please enter a number between {min_value} and {max_value}.")
else:
return value
except ValueError:
print("Error: Please enter a valid number.")
# Verwendung der Funktionen
quantity = get_positive_integer("Enter quantity: ")
print(f"Quantity: {quantity}")
grade = get_number_in_range("Enter grade (0-100): ", 0, 100)
print(f"Grade: {grade}")
temperature = get_number_in_range("Enter temperature (-50 to 50): ", -50, 50)
print(f"Temperature: {temperature}°C")Diese Funktionen kapseln die Validierungslogik, wodurch Ihr Hauptcode sauberer und besser lesbar wird. Sie sorgen außerdem für konsistentes Validierungsverhalten in Ihrem gesamten Programm.
26.4) Assertions für Invariantenprüfungen zur Entwicklungszeit verwenden
Assertions sind eine besondere Art von Prüfung, die während der Entwicklung verwendet wird, um zu verifizieren, dass die Annahmen Ihres Codes korrekt sind. Anders als Validierung (die erwartete Fehler von Benutzern oder externen Daten behandelt) fangen Assertions Programmierfehler ab – Situationen, die niemals passieren sollten, wenn Ihr Code korrekt ist.
26.4.1) Was Assertions sind und wann man sie verwenden sollte
Eine Assertion ist eine Anweisung, die zu einem bestimmten Zeitpunkt in Ihrem Code immer wahr sein sollte. Ist sie falsch, stimmt grundsätzlich etwas mit Ihrer Programmlogik nicht:
def calculate_average(numbers):
# Das sollte niemals passieren, wenn die Funktion korrekt aufgerufen wird
assert len(numbers) > 0, "numbers list cannot be empty"
return sum(numbers) / len(numbers)
# Korrekte Verwendung
grades = [85, 90, 78]
average = calculate_average(grades)
print(f"Average: {average:.1f}") # Output: Average: 84.3
# Falsche Verwendung – löst Assertion aus
empty_list = []
average = calculate_average(empty_list) # AssertionError: numbers list cannot be emptyWenn eine Assertion fehlschlägt, löst Python einen AssertionError mit Ihrer Nachricht aus. Das stoppt das Programm sofort und zeigt Ihnen genau, wo Ihre Annahme verletzt wurde.
Wichtige Unterscheidung:
- Validierung (mit
ifundraise): Zum Umgang mit erwarteten Problemen durch Benutzer oder externe Daten - Assertions: Zum Aufdecken von Programmierbugs während der Entwicklung
# Validierung – behandelt erwartete Benutzerfehler
def get_positive_number(prompt):
while True:
try:
value = float(input(prompt))
if value <= 0:
print("Error: Please enter a positive number.")
else:
return value
except ValueError:
print("Error: Please enter a valid number.")
# Assertion – fängt Programmierfehler ab
def calculate_discount(price, discount_rate):
# Diese sollten niemals verletzt werden, wenn das Programm korrekt geschrieben ist
assert price >= 0, "price should be non-negative"
assert 0 <= discount_rate <= 1, "discount_rate should be between 0 and 1"
return price * (1 - discount_rate)26.4.2) Funktions-Preconditions (Vorbedingungen) prüfen
Assertions sind hervorragend, um sicherzustellen, dass die Preconditions einer Funktion (Anforderungen, die wahr sein müssen, bevor die Funktion ausgeführt wird) erfüllt sind:
def get_list_element(items, index):
"""Get an element from a list at the specified index."""
# Preconditions
assert isinstance(items, list), "items must be a list"
assert isinstance(index, int), "index must be an integer"
assert 0 <= index < len(items), f"index {index} out of range for list of length {len(items)}"
return items[index]
# Korrekte Verwendung
numbers = [10, 20, 30, 40]
value = get_list_element(numbers, 2)
print(f"Value: {value}") # Output: Value: 30
# Programmierfehler – falscher Typ
value = get_list_element("not a list", 0) # AssertionError: items must be a list
# Programmierfehler – ungültiger Index
value = get_list_element(numbers, 10) # AssertionError: index 10 out of range for list of length 4Diese Assertions helfen dabei, Bugs während der Entwicklung zu finden. Wenn Sie versehentlich den falschen Typ oder einen ungültigen Index übergeben, sagt Ihnen die Assertion sofort, was schiefgelaufen ist.
26.4.3) Funktions-Postconditions (Nachbedingungen) prüfen
Postconditions sind Bedingungen, die nach der Ausführung einer Funktion wahr sein müssen. Assertions können verifizieren, dass Ihre Funktion gültige Ergebnisse produziert hat:
def calculate_percentage(part, whole):
"""Calculate what percentage 'part' is of 'whole'."""
# Preconditions
assert whole > 0, "whole must be positive"
assert part >= 0, "part must be non-negative"
# Prozent berechnen
percentage = (part / whole) * 100
# Postcondition – Ergebnis sollte ein gültiger Prozentwert sein
assert 0 <= percentage <= 100, f"percentage {percentage} is outside valid range"
return percentage
# Das funktioniert korrekt
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%") # Output: Percentage: 25.0%
# Das deckt einen Logikfehler in unserer Funktion auf
# (wir haben nicht geprüft, dass part <= whole)
percentage = calculate_percentage(150, 100) # AssertionError: percentage 150.0 is outside valid rangeDie Postcondition-Assertion hat einen Bug in unserer Funktion gefunden – wir haben vergessen zu validieren, dass part whole nicht überschreitet. Genau dafür sind Assertions da: Programmierfehler aufzudecken.
26.4.4) Assertions können deaktiviert werden
Ein wichtiges Merkmal von Assertions ist, dass sie deaktiviert werden können, wenn Python mit dem Flag -O (optimize) ausgeführt wird:
# Diese Datei heißt test_assertions.py
def divide(a, b):
assert b != 0, "divisor cannot be zero"
return a / b
result = divide(10, 2)
print(f"Result: {result}")
result = divide(10, 0) # AssertionError when assertions are enabledNormal ausführen:
python test_assertions.py
# Output: Result: 5.0
# Then: AssertionError: divisor cannot be zeroMit Optimierung ausführen:
python -O test_assertions.py
# Output: Result: 5.0
# Then: ZeroDivisionError: division by zeroDeshalb sollten Assertions niemals zur Validierung externer Daten verwendet werden – wenn jemand Ihr Programm mit -O ausführt, werden alle Assertions übersprungen. Verwenden Sie Assertions nur, um Programmierbugs während Entwicklung und Tests zu finden.