Ab hier geht es nicht mehr um Lesbarkeit, sondern um Laufzeit - das zweite Standbein von Kompetenzband D. Die Regel dazu ist kurz: Erst messen, dann ändern, dann nachmessen.
Ohne Messung ist es keine Optimierung, sondern eine Vermutung. Und Vermutungen über Laufzeit sind erstaunlich oft falsch - auch bei erfahrenen Entwicklerinnen und Entwicklern. Für D1A verlangt das Portfolio ausdrücklich eine Messung vorher und nachher.
timeit führt ein Stück Code mehrfach aus und misst die Zeit. Mehrfach deshalb, weil ein einzelner Durchlauf zu stark schwankt.
import timeit, random random.seed(42) gesperrt_liste = [f"user{i}" for i in range(10000)] gesperrt_set = set(gesperrt_liste) anfragen = [f"user{random.randrange(20000)}" for _ in range(2000)] def mit_liste(): return sum(1 for a in anfragen if a in gesperrt_liste) def mit_set(): return sum(1 for a in anfragen if a in gesperrt_set) assert mit_liste() == mit_set() # gleiches Ergebnis, sonst ist der Vergleich wertlos print(min(timeit.repeat(mit_liste, number=1, repeat=5))) print(min(timeit.repeat(mit_set, number=1, repeat=5)))
0.1070 # Liste 0.000068 # Set -> rund 1500-mal schneller
Drei Dinge machen die Messung erst brauchbar:
repeat und min: Der schnellste Durchlauf ist der aussagekräftigste, weil er am wenigsten durch andere Prozesse gestört wurde.assert auf gleiches Ergebnis. Eine schnellere Funktion, die etwas anderes liefert, ist keine Optimierung.Auf der Kommandozeile geht es auch ohne Skript:
python -m timeit -s "d = list(range(10000))" "9999 in d" python -m timeit -s "d = set(range(10000))" "9999 in d"
timeit vergleicht zwei Varianten, die Sie schon im Verdacht haben. cProfile sagt Ihnen, wo die Zeit überhaupt hingeht.
import cProfile, pstats cProfile.run("pruefen(anfragen)", "profil.stats") p = pstats.Stats("profil.stats") p.sort_stats("cumulative").print_stats(6)
12004 function calls in 0.104 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.002 0.002 0.104 0.104 prof_demo.py:12(pruefen)
3000 0.099 0.000 0.099 0.000 prof_demo.py:9(ist_gesperrt)
3000 0.002 0.000 0.003 0.000 prof_demo.py:6(normalisieren)
3000 0.001 0.000 0.001 0.000 {method 'strip' of 'str' objects}
So lesen Sie die Spalten:
| Spalte | Bedeutung |
|---|---|
ncalls | wie oft die Funktion aufgerufen wurde |
tottime | Zeit in der Funktion selbst, ohne Unterfunktionen |
cumtime | Zeit inklusive allem, was sie aufruft |
Der Befund ist eindeutig: ist_gesperrt verbraucht 0.099 der 0.104 Sekunden. normalisieren wird gleich oft aufgerufen und kostet 0.003 Sekunden. Wer ohne Profiling optimiert hätte, wäre gut über normalisieren gestolpert - die Funktion sieht teuer aus, strip und lower auf 3000 Strings. Sie ist es aber nicht.
Vorgehen bei Performance-Problemen
cProfile den Hotspot suchen, nicht raten.timeit nachmessen und das Ergebnis mit assert auf Gleichheit prüfen.
Eine oft zitierte Regel lautet: «Strings in einer Schleife mit += zusammenzuhängen ist quadratisch, nimm join.» Die Messung mit 20'000 Teilstücken:
s += t 0.00060 s "".join() 0.00012 s -> Faktor 5, nicht Faktor 1000
CPython optimiert genau dieses Muster intern, wenn der String nirgends sonst referenziert wird. join bleibt trotzdem die bessere Wahl - weil es kürzer und klarer ist und in anderen Python-Implementationen zuverlässig schneller. Aber die Begründung lautet Lesbarkeit, nicht Faktor 1000.
Genau das ist der Unterschied zwischen Niveau I und Niveau A: Auf Niveau I setzen Sie eine vorgegebene Massnahme um. Auf Niveau A prüfen Sie nach, ob sie in Ihrem Fall überhaupt etwas bringt - und sagen es ehrlich, wenn nicht.