Die Techniken aus LU07c gelten in jeder Sprache und jedem Paradigma. Diese Seite behandelt die drei Umbauten, die für dieses Modul zentral sind: von der Schleife zur Pipeline, von unpure zu pure, von mutable zu immutable.
Das Ziel: Nicht «funktional, weil funktional». Sondern: Code, dessen Ergebnis nur von den Eingaben abhängt, lässt sich lesen, ohne den Rest des Programms zu kennen - und testen, ohne ihn vorher in einen bestimmten Zustand zu bringen.
Sehr viele Schleifen tun in Wahrheit nur eines von drei Dingen: filtern, umformen oder zusammenfassen. Genau dafür gibt es Comprehensions und map/filter/reduce (LU04).
# vorher namen = [] for e in eintraege: namen.append(e["name"].upper()) # nachher namen = [e["name"].upper() for e in eintraege]
# vorher aktive = [] for e in eintraege: if e["aktiv"]: aktive.append(e) # nachher aktive = [e for e in eintraege if e["aktiv"]]
# vorher total = 0 for e in eintraege: total += e["betrag"] # nachher total = sum(e["betrag"] for e in eintraege)
Der Unterschied ist nicht die Zeilenzahl. Die Vorher-Version sagt wie gerechnet wird (Zähler anlegen, durchlaufen, addieren), die Nachher-Version sagt was herauskommen soll. Das ist der Schritt von imperativ zu deklarativ aus LU01a.
Eine typische Auswertung, wie sie in einem Flask-Projekt vorkommt: Umsatz aller Zubehör-Positionen.
verkaeufe = [ {"artikel": "Maus", "kategorie": "Zubehoer", "menge": 3, "preis": 25.0}, {"artikel": "Tastatur", "kategorie": "Zubehoer", "menge": 1, "preis": 80.0}, {"artikel": "Laptop", "kategorie": "Geraet", "menge": 2, "preis": 1200.0}, {"artikel": "USB-Hub", "kategorie": "Zubehoer", "menge": 5, "preis": 12.0}, {"artikel": "Monitor", "kategorie": "Geraet", "menge": 1, "preis": 210.0}, ]
Vorher - eine Schleife, die filtert, rechnet und aufsummiert:
def umsatz_zubehoer(daten): total = 0.0 for eintrag in daten: if eintrag["kategorie"] == "Zubehoer": total = total + eintrag["menge"] * eintrag["preis"] return round(total, 2)
Nachher - die drei Aufgaben sind getrennt und einzeln benannt:
def ist_zubehoer(eintrag): return eintrag["kategorie"] == "Zubehoer" def zeilenumsatz(eintrag): return eintrag["menge"] * eintrag["preis"] def umsatz_zubehoer(daten): return round(sum(zeilenumsatz(e) for e in daten if ist_zubehoer(e)), 2)
umsatz_zubehoer(verkaeufe) -> 215.0 (beide Versionen)
ist_zubehoer und zeilenumsatz sind jetzt wiederverwendbar - für die Sortierung, den Filter im Frontend, die Statistikseite. Aus einer Auswertung sind Bausteine geworden.
Mit filter und reduce geschrieben, liefert dieselbe Logik denselben Wert:
from functools import reduce round(reduce(lambda a, e: a + zeilenumsatz(e), filter(ist_zubehoer, verkaeufe), 0.0), 2) # -> 215.0
Welche der drei Fassungen nehmen? In Python ist die Comprehension mit sum() die lesbarste und im Zweifel die richtige Wahl. map/filter/reduce zeigen Sie im Portfolio dort, wo die Kompetenz C2 das verlangt - nicht überall.
Auslöser: globaler Zustand, global-Schlüsselwort, Funktion verändert ihre Argumente.
# vorher - unpure warenkorb = [] def hinzufuegen(artikel): global warenkorb warenkorb.append(artikel) print(f"{artikel} hinzugefügt")
Diese Funktion ist nicht testbar, ohne vorher warenkorb zu präparieren, und sie tut drei Dinge gleichzeitig: rechnen, Zustand ändern, ausgeben.
# nachher - funktionaler Kern, dünne Schale def mit_artikel(korb, artikel): # pure: neue Liste, kein Seiteneffekt return korb + [artikel] def hinzufuegen_und_melden(korb, artikel): # Schale: hier ist der Seiteneffekt print(f"{artikel} hinzugefügt") return mit_artikel(korb, artikel)
Das Muster heisst funktionaler Kern, imperative Schale: Die Berechnung ist pure und getestet, Ein- und Ausgabe (print, Datenbank, HTTP-Response) leben am Rand. In Flask heisst das konkret: Die Route nimmt Request-Daten entgegen und gibt eine Response zurück, die Berechnung dazwischen steckt in pure Funktionen, die man ohne laufenden Server testen kann.
Auslöser: eine Funktion verändert eine Liste oder ein Dict, das sie als Argument bekommen hat.
# vorher - die Eingabe wird verändert def preise_erhoehen(produkte, prozent): for p in produkte: p["preis"] = round(p["preis"] * (1 + prozent / 100), 2) return produkte # nachher - neue Daten, Original bleibt def preise_erhoehen(produkte, prozent): return [ {**p, "preis": round(p["preis"] * (1 + prozent / 100), 2)} for p in produkte ]
Die Vorher-Version ist der Klassiker aus LU02d: Weil Listen und Dicts by reference übergeben werden, sieht der Aufrufer die Änderung, ob er will oder nicht. Wer die Preisliste danach noch im Originalzustand braucht, hat ein Problem, das erst Wochen später auffällt.
Nicht jede Schleife wird als Pipeline besser. Lassen Sie die Schleife stehen, wenn:
break) - dafür gibt es next(), any() und all(), aber nicht jede Variante ist lesbarer,Eine dreifach verschachtelte Comprehension ist kein funktionaler Fortschritt, sondern ein neuer Smell.
ist_zubehoer, zeilenumsatz).