Einführung
Spaghetti-Code ist schwer zu handhaben, da seine Logik verworren ist. Eine Funktion in Python kann mehrere zusammenhängende Schritte ausführen und dennoch gut lesbar bleiben. Probleme treten auf, wenn verschiedene Verantwortlichkeiten eng miteinander verbunden sind, Abhängigkeiten unklar sind und eine Änderung eines Logikstücks erfordert, dass man durch nicht verwandte Teile des Codes zurückverfolgen muss.
Das Aufteilen von Code in fokussierte Funktionen kann helfen, diese Komplexität zu reduzieren. Eine gute Python-Funktion sollte einen klaren Zweck haben, gut definierte Eingaben akzeptieren und ein verständliches Ergebnis liefern. Dies macht einzelne Teile leichter lesbar, testbar, debugbar und modifizierbar.
Python bietet viel Freiheit in der Strukturierung des Codes, was diese Gewohnheiten besonders wichtig macht. Zu lernen, wie man Verantwortlichkeiten trennt und die Beziehungen zwischen Logikstücken klar hält, ist ein praktischer Weg, um zu saubererem und wartbarem Python zu gelangen. Für eine detaillierte Anleitung zur Effizienten Bereinigung von CSV-Dateien mit Python können Sie unseren Leitfaden lesen.
Dieser Artikel behandelt:
- Wie unordentlicher, verworrener Code aussieht, anhand eines Beispiels, das Sie tatsächlich ausführen können
- Wie man eine Funktion in kleine, fokussierte Teile aufteilt
- Wie man Bestelldaten mit einer Datenklasse anstelle eines Wörterbuchs modelliert
- Wie man Fehler auslöst, anstatt stillschweigend Warnungen auszugeben
- Wie man die resultierenden Funktionen unabhängig testet und das gleiche Muster auf den eigenen Code anwendet
Wir werden ein Skript Schritt für Schritt von seinem nicht so wartbaren Zustand zu einer saubereren Version durchgehen.
Der Code ist auf GitHub verfügbar.
Erkennen der Anzeichen von unordentlichem Code
Hier ist eine kleine Funktion zur Auftragsbearbeitung für einen Online-Shop. Sie berechnet einen Rabatt, aktualisiert den Lagerbestand und sendet eine E-Mail, alles innerhalb einer Funktion.
inventory = {"sku-1042": 18, "sku-2077": 4}
def process_order(order):
total = 0
for item in order["items"]:
price = item["unit_price"] * item["quantity"]
if order["customer_type"] == "vip":
price = price * 0.85
elif order["customer_type"] == "regular" and total > 100:
price = price * 0.95
total += price
if item["sku"] in inventory:
inventory[item["sku"]] -= item["quantity"]
else:
print(f"Warning: {item['sku']} not found in inventory")
if total > 500:
shipping = 0
else:
shipping = 12.99
total += shipping
print(f"Sending confirmation email to {order['customer_email']}")
print(f"Order total: ${total:.2f}")
return total
Die Funktion process_order berechnet Preise, wendet einen Rabatt an, verändert das globale Wörterbuch inventory, entscheidet über den Versand und simuliert das Senden einer E-Mail – alles in derselben Schleife. Zudem gibt es einen Fehler: Die Überprüfung des Rabatts für reguläre Kunden erfolgt während der Schleife, sodass es davon abhängt, in welcher Reihenfolge die Artikel erscheinen, ob ein Kunde den Rabatt erhält oder nicht. Solche Fehler sind leicht zu übersehen, da alles vermischt ist.
Hier sind die Anzeichen, auf die Sie in Ihrem eigenen Code achten sollten:
- Eine Funktion, deren Name nicht zu allem passt, was sie tut
- Eine Variable, die ihre Bedeutung ändert, während man die Funktion hinuntergeht
- Berechnungen, die von der Reihenfolge abhängen, in der die Anweisungen ausgeführt werden
Eine Funktion in fokussierte Teile aufteilen
Geben Sie jeder Verantwortung ihre eigene Funktion, mit klaren Eingaben und einem klaren Rückgabewert. Keine Veränderung des gemeinsamen Zustands innerhalb einer Schleife und keine Berechnung, die von der Ausführungsreihenfolge abhängt.
def calculate_subtotal(items):
return sum(item.unit_price * item.quantity for item in items)
def apply_discount(subtotal, customer_type):
if customer_type == "vip":
return subtotal * 0.85
if customer_type == "regular" and subtotal > 100:
return subtotal * 0.95
return subtotal
def calculate_shipping(discounted_total):
return 0.0 if discounted_total > 500 else 12.99
Jede Funktion hier nimmt einfache Werte entgegen und gibt einen einfachen Wert zurück. apply_discount überprüft nun den endgültigen Zwischensumme anstelle eines laufenden Totals, was den Fehler aufgrund der Reihenfolge direkt beseitigt, da die Berechnung von der Schleife getrennt ist. Sie können jede dieser drei Funktionen unabhängig aufrufen und genau wissen, was sie tut, ohne den Rest des Skripts auszuführen.
Wörterbücher durch eine Datenklasse ersetzen
Das Übergeben von Wörterbüchern mit String-Schlüsseln funktioniert, garantiert jedoch nicht, welche Felder existieren oder welchen Typ sie haben. Datenklassen beheben dies, indem sie der Bestellung und ihren Artikeln eine definierte Struktur geben.
from dataclasses import dataclass
@dataclass
class OrderItem:
sku: str
unit_price: float
quantity: int
@dataclass
class Order:
customer_email: str
customer_type: str
items: list[OrderItem]
Mit diesen Klassen können die verbleibenden Teile gegen eine bekannte Struktur geschrieben werden, anstatt die Schlüssel des Wörterbuchs zu erraten:
def process_order(order: Order, inventory: dict) -> float:
subtotal = calculate_subtotal(order.items)
discounted = apply_discount(subtotal, order.customer_type)
total = discounted + calculate_shipping(discounted)
update_inventory(order.items, inventory)
return total
Die Funktion process_order ist nun ein Koordinator anstelle eines Arbeiters; sie ruft jeden Schritt in der Reihenfolge auf und gibt das Ergebnis zurück. Das Lesen von oben nach unten erzählt die gesamte Geschichte der Auftragsbearbeitung: berechnen, rabattieren, versenden, Lagerbestand aktualisieren.
Fehler auslösen anstelle von Warnungen auszugeben
Die ursprüngliche Funktion gab eine Warnung aus, wenn eine SKU nicht gefunden wurde, und fuhr fort. Das bedeutet, dass eine fehlende SKU nichts tatsächlich stoppt; sie protokolliert nur eine Zeile, die in einem geschäftigen Terminal leicht übersehen werden kann.
def update_inventory(items, inventory):
for item in items:
if item.sku not in inventory:
raise ValueError(f"{item.sku} not found in inventory")
inventory[item.sku] -= item.quantity
Das Auslösen einer Ausnahme macht den Fehler an dem Punkt, an dem er auftritt, explizit. Dies verhindert, dass die Bestellung fortgesetzt wird, wenn das Update des Lagerbestands nicht erfolgreich abgeschlossen wurde. Es erleichtert auch die Erkennung des Problems während des Testens und das Nachverfolgen beim Debuggen.
Jedes Stück unabhängig testen
Sobald die Logik in kleine Funktionen aufgeteilt ist, erfordert das Testen dieser Funktionen nicht mehr, dass die gesamte Pipeline ausgeführt wird:
def test_apply_discount_vip():
assert apply_discount(200, "vip") == 170.0
def test_apply_discount_regular_under_threshold():
assert apply_discount(80, "regular") == 80
Sie können auch pytest verwenden, um dies direkt zu machen. Wenn apply_discount fehlschlägt, zeigt der fehlerhafte Test direkt auf die Rabattregel. Vergleichen Sie das mit der ursprünglichen Funktion, bei der ein Fehlerbericht nur sagen würde, dass die Gesamtsumme der Bestellung falsch aussieht, ohne anzugeben, welches ihrer vier Verantwortlichkeiten schuld war.
Das Hinzufügen von Typ-Hinweisen zu diesen Funktionen, wie oben in process_order gezeigt, erweitert dies weiter – ein Linter kann einen Aufrufer, der ein Wörterbuch übergibt, wo ein Order erwartet wird, erfassen, bevor der Code überhaupt ausgeführt wird.
Übertragung auf den eigenen Code
Das Muster in diesem Tutorial gilt für jede Funktion, die über eine Aufgabe hinausgewachsen ist. Das nächste Mal, wenn Sie eine Funktion öffnen, die Sie vermeiden, arbeiten Sie sie in dieser Reihenfolge durch:
- Listen Sie jede einzelne Aufgabe auf, die die Funktion ausführt, in einfacher Sprache, ein Punkt pro Zeile.
- Ziehen Sie jeden Punkt in seine eigene Funktion, die einfache Argumente annimmt und einen einfachen Wert zurückgibt.
- Ersetzen Sie jedes Wörterbuch, das herumgereicht wird, durch eine Datenklasse, sodass die Struktur der Daten explizit ist.
- Ersetzen Sie die Fehlerbehandlung mit Ausgaben durch eine Ausnahme, die die Ausführung stoppt.
- Schreiben Sie einen Test für jede extrahierte Funktion, bevor Sie zur nächsten übergehen.
Indem Sie dies für eine Funktion nach der anderen tun, anstatt eine ganze Datei auf einmal neu zu schreiben, bleibt die Änderung überprüfbar und das Skript funktioniert in jedem Schritt.
Zusammenfassung
Hier ist eine schnelle Referenz für die Änderungen, die in diesem Tutorial behandelt wurden, und was jede von ihnen Ihnen bietet:
- Problem im ursprünglichen Code: Eine Funktion, die mehrere nicht verwandte Verantwortlichkeiten behandelt.
- Angewandte Lösung: Teilen Sie die Funktion in kleinere auf, jede mit einer Verantwortung.
- Was es Ihnen gibt: Jedes Stück kann unabhängig gelesen, geändert und getestet werden.
- Problem im ursprünglichen Code: Eine Berechnung, die von der Reihenfolge abhängt, in der die Anweisungen ausgeführt werden.
- Angewandte Lösung: Basieren Sie die Berechnung auf einem endgültigen Wert anstelle eines sich während der Schleife ändernden.
- Was es Ihnen gibt: Entfernt Fehler, die durch die Ausführungsreihenfolge und nicht durch die tatsächliche Logik verursacht werden.
- Problem im ursprünglichen Code: Daten, die als lockeres Wörterbuch übergeben werden.
- Angewandte Lösung: Modellieren Sie die Daten mit einer Datenklasse.
- Was es Ihnen gibt: Macht die verfügbaren Felder und Typen explizit und lässt einen Linter Inkonsistenzen erfassen.
- Problem im ursprünglichen Code: Ein Fehler, der mit
printprotokolliert wurde, während die Ausführung fortfuhr. - Angewandte Lösung: Lösen Sie eine Ausnahme aus.
- Was es Ihnen gibt: Hebt das Problem sofort hervor, anstatt die Ausführung fortzusetzen.
- Problem im ursprünglichen Code: Keine Möglichkeit, ein Stück Logik zu testen, ohne das gesamte Skript auszuführen.
- Angewandte Lösung: Fügen Sie einen fokussierten Test für jede extrahierte Funktion hinzu.
- Was es Ihnen gibt: Ein fehlerhafter Test zeigt direkt auf das defekte Stück.
Weiterführende Literatur:
- Die Kunst, lesbare Python-Funktionen zu schreiben
- Python-Tests mit pytest
- Refactoring von Python-Anwendungen zur Vereinfachung
Viel Spaß beim Programmieren!
Bala Priya C ist Entwicklerin und technische Autorin aus Indien. Sie arbeitet an der Schnittstelle von Mathematik, Programmierung, Datenwissenschaft und Inhaltserstellung. Ihre Interessens- und Fachgebiete umfassen DevOps, Datenwissenschaft und natürliche Sprachverarbeitung. Sie liest, schreibt, programmiert und genießt Kaffee! Derzeit arbeitet sie daran, ihr Wissen mit der Entwicklergemeinschaft zu teilen, indem sie Tutorials, Anleitungen, Meinungsartikel und mehr verfasst. Bala erstellt auch ansprechende Übersichten und Programmieranleitungen.
„`
Quellen: kdnuggets
Bildquelle: KI generiert
Dieser Text wurde mit Hilfe von künstlicher Intelligenz in Zusammenarbeit mit unserer Redaktion erstellt.
🚀