Appearance
Kapitel 19: Python Leistungsoptimierung
🎯 Lernziele
In diesem Kapitel wirst du:
- Grundlegendes zur Python-Leistung verstehen
- Profiling-Tools verwenden, um Flaschenhälse zu finden
- Code-Optimierungstechniken erlernen
- Speicherverwaltung und Garbage Collection verstehen
19.1 Grundlegendes zur Python-Leistung
🐢 Warum ist Python "langsam"?
- Interpretierte Sprache: Python wird zur Laufzeit interpretiert, nicht kompiliert
- GIL (Global Interpreter Lock): Erlaubt nur einem Thread, den Python-Interpreter gleichzeitig auszuführen
- Dynamische Typisierung: Typüberprüfungen erfolgen zur Laufzeit
- Speicherverwaltung: Automatische Speicherbereinigung (Garbage Collection)
⚡ Wann ist Python "schnell genug"?
- ✅ Für die meisten Anwendungen ist Python schnell genug
- ✅ Die meisten Engpässe liegen bei I/O-Operationen, nicht bei der CPU
- ✅ Optimiere nur, wenn es notwendig ist (Premature Optimization)
- ✅ "Erst funktionsfähig machen, dann optimieren"
19.2 Profiling - Flaschenhälse finden
⏱️ Zeitmessung mit time-Modul
python
import time
def zeitmessung(funktion, *args, **kwargs):
"""Misst die Ausführungszeit einer Funktion"""
start = time.time()
ergebnis = funktion(*args, **kwargs)
ende = time.time()
print(f"{funktion.__name__}: {ende - start:.6f} Sekunden")
return ergebnis
# Beispiel
def langsame_funktion():
summe = 0
for i in range(1000000):
summe += i
return summe
zeitmessung(langsame_funktion)🔍 Profiling mit cProfile
python
import cProfile
def langsame_funktion():
summe = 0
for i in range(1000000):
summe += i
return summe
# Profiling
cProfile.run('langsame_funktion()')Ausgabe:
4 function calls in 0.050 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.050 0.050 0.050 0.050 <ipython-input-...>:1(langsame_funktion)
1 0.000 0.000 0.050 0.050 <string>:1(<module>)
1 0.000 0.000 0.050 0.050 {builtin.exec}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}📊 line_profiler (Zeilenweises Profiling)
bash
# Installation
pip install line_profilerpython
# meiene_datei.py
@profile # Dekorator für line_profiler
def meiene_funktion():
summe = 0
for i in range(1000):
summe += i
return summe
if __name__ == "__main__":
meiene_funktion()bash
# Ausführen mit line_profiler
kernprof -l -v meiene_datei.py19.3 Code-Optimierungstechniken
🚀 1. Listen-Abstraktion (List Comprehension)
Langsam (for-Schleife):
python
quadrate = []
for i in range(1000):
quadrate.append(i ** 2)Schnell (List Comprehension):
python
quadrate = [i ** 2 for i in range(1000)]🔢 2. join() für String-Verknüpfung verwenden
Langsam (String-Verknüpfung mit +):
python
string = ""
for i in range(1000):
string += str(i) # Jedes Mal wird ein neuer String erstellt!Schnell (join()):
python
string = "".join([str(i) for i in range(1000)])🔍 3. Lokale Variablen verwenden
Lokale Variablen sind schneller als globale:
python
# Langsam (Zugriff auf globale Variable)
globale_variable = 10
def funktion1():
for i in range(1000000):
x = globale_variable # Langsam
# Schnell (lokale Variable)
def funktion2():
lokale_variable = 10
for i in range(1000000):
x = lokale_variable # Schnell📋 4. sort() anstelle von sorted() verwenden
python
# In-place Sortierung (schneller, verändert Originaliste)
liste = [3, 1, 4, 1, 5, 9, 2]
liste.sort()
# Neue Liste erstellen (langsamer, Originaliste bleibt unverändert)
liste = [3, 1, 4, 1, 5, 9, 2]
neue_liste = sorted(liste)🔁 5. Generatoren verwenden (Speicherplatzeinsparung)
python
# Speicherintensiv (Liste)
def gete_zahlen_liste(n):
result = []
for i in range(n):
result.append(i)
return result
# Speichereffizient (Generator)
def gete_zahlen_generator(n):
for i in range(n):
yield i
# Vergleich
%timeit gete_zahlen_liste(1000000) # Langsam, viel Speicher
%timeit list(gete_zahlen_generator(1000000)) # Schnell, wenig Speicher📦 6. collections-Modul verwenden
python
from collections import Counter, defaultdict
# Häufigkeiten zählen (manuell)
text = "hallo welt hallo"
wörter = text.split()
häufigkeit = {}
for wort in wörter:
if wort in häufigkeit:
häufigkeit[wort] += 1
else:
häufigkeit[wort] = 1
# Häufigkeiten zählen (mit Counter)
häufigkeit = Counter(wörter)
# Standardwerte in Wörterbüchern
# Manuell
wörterbuch = {}
if "a" not in wörterbuch:
wörterbuch["a"] = []
wörterbuch["a"].append(1)
# Mit defaultdict
wörterbuch = defaultdict(list)
wörterbuch["a"].append(1)19.4 Speicherverwaltung und Garbage Collection
🗑️ Wie Python Speicher verwaltet
- Referenzzählung: Python verfolgt, wie viele Referenzen auf ein Objekt zeigen
- Garbage Collection (GC): Entfernt Objekte mit Referenzanzahl 0
- Generationsbasierte GC: Objekte, die länger überleben, werden seltener überprüft
📏 Speicherverbrauch überprüfen mit sys.getsizeof()
python
import sys
liste = [1, 2, 3, 4, 5]
print(sys.getsizeof(liste)) # Gibt die Größe des Liste-Objekts in Bytes aus
# Verschiedene Datentypen
print(sys.getsizeof(1)) # int
print(sys.getsizeof(1.0)) # float
print(sys.getsizeof("hallo")) # str
print(sys.getsizeof([1, 2, 3])) # list
print(sys.getsizeof((1, 2, 3))) # tuple🔄 Garbage Collection manuell steuern
python
import gc
# Garbage Collection manuell auslösen
gc.collect()
# GC deaktivieren (Vorsicht!)
gc.disable()
# GC aktivieren
gc.enable()
# Schwellenwerte für GC anzeigen
print(gc.get_threshold())
# Schwellenwerte für GC festlegen
gc.set_threshold(700, 10, 10)19.5 C-Erweiterungen und JIT-Compiler
⚙️ Cython verwenden (Python zu C kompilieren)
python
# meiene_cython_datei.pyx
def schwere_berechnung(int n):
cdef int i
cdef double summe = 0
for i in range(n):
summe += i * i
return summe🚀 Numba verwenden (JIT-Compiler)
python
from numba import jit
@jit(nopython=True)
def schwere_berechnung(n):
summe = 0
for i in range(n):
summe += i * i
return summe
# Beim ersten Aufruf kompiliert Numba die Funktion
result = schwere_berechnung(1000000)
# Beim zweiten Aufruf ist die Funktion bereits kompiliert (sehr schnell)
result = schwere_berechnung(1000000)🔥 PyPy verwenden (Alternative Python-Implementierung mit JIT)
bash
# PyPy installieren (von der Webseite)
# Dann Python-Code mit PyPy ausführen
pypy meiene_datei.py19.6 Praxisbeispiel: Optimierung eines Sortieralgorithmus
🐌 Unoptimierter Bubble Sort
python
def bubble_sort(liste):
n = len(liste)
for i in range(n):
for j in range(0, n-i-1):
if liste[j] > liste[j+1]:
liste[j], liste[j+1] = liste[j+1], liste[j]
return liste
# Test
import random
liste = [random.randint(1, 1000) for _ in range(1000)]
%timeit bubble_sort(liste.copy())🏎️ Optimierter Quick Sort (eingebaut sorted())
python
def quick_sort(liste):
return sorted(liste)
# Test
import random
liste = [random.randint(1, 1000) for _ in range(1000)]
%timeit quick_sort(liste.copy())📊 Vergleich der Laufzeiten
python
import timeit
# Bubble Sort
bubble_zeit = timeit.timeit(
"bubble_sort(liste.copy())",
setup="from __main__ import bubble_sort, liste",
number=10
)
# Quick Sort
quick_zeit = timeit.timeit(
"quick_sort(liste.copy())",
setup="from __main__ import quick_sort, liste",
number=10
)
print(f"Bubble Sort: {bubble_zeit:.4f} Sekunden")
print(f"Quick Sort: {quick_zeit:.4f} Sekunden")
print(f"Geschwindigkeitsunterschied: {bubble_zeit/quick_zeit:.2f}x")⚠️ Häufige Fehler
❌ Fehler 1: Premature Optimization
python
# Schlecht: Code wird für minimale Optimierung unleserlich gemacht
def schlechter_code(n):
result = [i**2 for i in range(n) if i%2==0][::2][:10] # Unleserlich!
return result
# Besser: Erst leserlich machen, dann optimieren (nur wenn nötig)
def besserer_code(n):
result = []
for i in range(n):
if i % 2 == 0:
result.append(i**2)
return result[::2][:10]❌ Fehler 2: Optimierung ohne Profiling
python
# Schlecht: Code wird "optimiert", ohne zu wissen, wo der Flaschenhals liegt
def langsame_funktion():
# ... (unbekannter Flaschenhals)
pass
# Optimiere irgendwo...
def "optimierte" _funktion():
# ... (vielleicht am falschen Ort optimiert)
pass
# Besser: Zuerst profilen!
import cProfile
cProfile.run('langsame_funktion()')
# Dann am Flaschenhals optimieren❌ Fehler 3: Speicheroptimierung ohne Notwendigkeit
python
# Schlecht: Code wird komplex für minimale Speicherersparnis
def komplexer_code():
# Verwendung von Generatoren für kleine Datenmengen
return (i**2 for i in range(10)) # Generator
# Besser: Einfachheit genießenen, wenn Speicher nicht knapp ist
def einfacher_code():
return [i**2 for i in range(10)] # Liste📝 Zusammenfassung
In diesem Kapitel hast du gelernt:
- ✅ Grundlegendes zur Python-Leistung zu verstehen
- ✅ Profiling-Tools zu verwenden (
time,cProfile,line_profiler) - ✅ Code-Optimierungstechniken anzuwenden (List Comprehensions,
join(), lokale Variablen, Generatoren) - ✅ Speicherverwaltung und Garbage Collection zu verstehen
- ✅ C-Erweiterungen und JIT-Compiler zu verwenden (
Cython,Numba,PyPy)
🎯 Übung
- Schreibe ein Programm, das die Fibonacci-Folge auf drei verschiedene Arten berechnet (rekursiv, iterativ, mit Memoisierung) und miss ihre Laufzeiten
- Optimiere ein Programm, das eine große Textdatei liest und die Häufigkeit jedes Wortes zählt
- Schreibe ein Programm, das eine große Liste von Zahlen sortiert, und vergleiche die Laufzeiten von
bubble_sort,insertion_sortundsorted() - Verwende
Numba, um eine rechenintensive Funktion zu beschleunigen
⏭️ Nächstes Kapitel
In Kapitel 20 werden wir häufige Fehler und Fallen in Python behandeln - wie man sie vermeidet und behebt!
