Skip to content

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"?

  1. Interpretierte Sprache: Python wird zur Laufzeit interpretiert, nicht kompiliert
  2. GIL (Global Interpreter Lock): Erlaubt nur einem Thread, den Python-Interpreter gleichzeitig auszuführen
  3. Dynamische Typisierung: Typüberprüfungen erfolgen zur Laufzeit
  4. 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_profiler
python
# 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.py

19.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

  1. Referenzzählung: Python verfolgt, wie viele Referenzen auf ein Objekt zeigen
  2. Garbage Collection (GC): Entfernt Objekte mit Referenzanzahl 0
  3. 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.py

19.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

  1. Schreibe ein Programm, das die Fibonacci-Folge auf drei verschiedene Arten berechnet (rekursiv, iterativ, mit Memoisierung) und miss ihre Laufzeiten
  2. Optimiere ein Programm, das eine große Textdatei liest und die Häufigkeit jedes Wortes zählt
  3. Schreibe ein Programm, das eine große Liste von Zahlen sortiert, und vergleiche die Laufzeiten von bubble_sort, insertion_sort und sorted()
  4. 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!

Frei für alle Anfänger