Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Profiling in Python

In dieser Session schauen wir uns an, wie wir die Laufzeit von Python-Code messen können. Profiling hilft uns zu verstehen, wo unser Programm Zeit verbringt, damit wir gezielt optimieren können – und nicht nur raten.


Warum Profiling wichtig ist

In vielen Python- und Data-Science-Projekten arbeiten wir mit großen Datensätzen oder rechenintensiven Algorithmen. Manchmal ist der Code „schnell genug“, manchmal ganz offensichtlich nicht. Profiling hilft dir zu entscheiden:

Typische Fehler ohne Profiling:

Faustregel: Nicht raten, wie schnell etwas ist – erst messen, dann optimieren.


Das Modul time verwenden

Das Modul time bietet einfache Funktionen, um Zeiten zu messen. Es ist oft das erste Werkzeug, wenn man nur ein grobes Gefühl für die Laufzeit bekommen möchte.

Ein typisches Muster:

import time

def integer_sum(count_to: int):
    total = 0
    for i in range(int(count_to)):
        total += i
    return total

start_time = time.time()
integer_sum(1_000_000)
end_time = time.time()

print(f"Execution time: {end_time - start_time} seconds")

Erklärung:

Einschränkungen:


Das Modul timeit verwenden

Das Modul timeit ist für zuverlässige Micro-Benchmarks gedacht. Es:

Ein einfaches Beispiel:

import timeit

code_to_test = """
total = 0
for i in range(int(1e5)):
    total += i
"""

execution_time = timeit.timeit(stmt=code_to_test, number=100)
print(f"Average execution time: {execution_time / 100:.6f} seconds")

Erklärung:

Beispiel mit Funktion statt String:

import timeit

code_to_test = """
total = 0
for i in range(int(1e5)):
    total += i
"""

execution_time = timeit.timeit(stmt=code_to_test, number=100)
print(f"Average execution time: {execution_time / 100:.6f} seconds")

Vorteile:


Jupyter-Notebook-Magics

In Jupyter-Notebooks gibt es praktische Magic-Befehle, die time und timeit für dich kapseln:

Line- vs. Cell-Magics

%timeit (und %%timeit) wird:

Du kannst die Anzahl der Durchläufe steuern:

%%timeit -n 10 -r 5

integer_sum(1000_000)

Insgesamt wird die Funktion hier also 10 * 5 = 50 Mal ausgeführt.


Praxisaufgabe: Minimum in einer Liste finden

Jetzt wenden wir diese Werkzeuge auf eine einfache, aber typische Aufgabe an:

Finde den kleinsten Wert in einer großen Liste von Zufallszahlen.

Wir implementieren drei Methoden:

  1. Eine manuelle for-Schleife,

  2. Die eingebaute Funktion min(),

  3. Eine Lösung auf Basis von NumPy.

Anschließend messen und vergleichen wir die Laufzeiten.

Setup: Erzeugen einer großen Zufallsliste

import random

random.seed(42)  # Experiment reproduzierbar machen

random_list = [random.randint(0, 1_000_000) for _ in range(1_000_000)]

Erklärung:


Methode 1: For-Schleife

def find_min_loop(lst):
    min_value = lst[0]
    for num in lst:
        if num < min_value:
            min_value = num
    return min_value

Erklärung:


Methode 2: Eingebaute Funktion min()

def find_min_builtin(lst):
    return min(lst)

Erklärung:


Methode 3: NumPy verwenden

import numpy as np

def find_min_numpy(lst):
    np_array = np.array(lst)
    return np.min(np_array)

Erklärung:

Aber: Wir müssen den Konvertierungsaufwand berücksichtigen:


Performance vergleichen

Jetzt verwenden wir timeit, um die drei Ansätze zu messen.

Hinweis: Im Notebook kann man stattdessen %%timeit verwenden. Hier nutzen wir timeit.timeit(), damit der Code auch als Skript funktioniert.

Stelle sicher, dass die Funktionen und random_list bereits definiert sind.

Messung Methode 1: For-Schleife

import timeit

execution_time_loop = timeit.timeit(
    stmt='find_min_loop(random_list)',
    setup='from __main__ import find_min_loop, random_list',
    number=10,
)

print(f"Average execution time (loop): {execution_time_loop / 10:.6f} seconds")

Messung Methode 2: Eingebaute Funktion min()

execution_time_builtin = timeit.timeit(
    stmt='find_min_builtin(random_list)',
    setup='from __main__ import find_min_builtin, random_list',
    number=10,
)

print(f"Average execution time (builtin): {execution_time_builtin / 10:.6f} seconds")

Messung Methode 3: NumPy

execution_time_numpy = timeit.timeit(
    stmt='find_min_numpy(random_list)',
    setup='from __main__ import find_min_numpy, random_list, np',
    number=10,
)

print(f"Average execution time (NumPy): {execution_time_numpy / 10:.6f} seconds")

Ergebnisse interpretieren

Typischerweise wirst du etwas in dieser Art beobachten:

Wichtige Erkenntnis:

Alle drei Methoden haben die gleiche asymptotische Komplexität O(n), aber Konstanten machen einen großen Unterschied – und in C implementierte Built-ins sind oft deutlich schneller als reines Python.


Fortgeschrittenes Profiling mit cProfile und snakeviz

Timing ist super, wenn du ganze Funktionen oder Alternativen vergleichen möchtest. Aber was, wenn du ein größeres Programm hast und wissen willst, welche inneren Funktionen langsam sind?

Dafür nutzen wir Profiler wie cProfile, die zeigen:

Mit snakeviz können wir die Ergebnisse dann visualisieren.


Den Beispielcode verstehen

Wir profilieren folgendes Skript, das grob den Ablauf simuliert:

import random
import time

def generate():
    data = [random.randint(0, 99) for _ in range(1, 100000)]
    data = validate(data)
    return data

def validate(data, chance_percent=50):
    data = [x for x in data if random.randint(1, 100) > chance_percent]
    return data

def search_function(data, targets=[42, 17]):
    counter = 0
    for value in data:
        if value in targets:
            counter += 1
    return counter

def ascii_renderer(result):
    message = f"I found the targets {result} times!"
    for char in message:
        time.sleep(0.01)
        print(char, end="")
    print()  # For newline after the message.

def main():
    data = generate()
    result = search_function(data)
    ascii_renderer(result)

if __name__ == "__main__":
    main()

Was machen die Funktionen?


Docstrings ergänzen und Code aufräumen

Wir versehen den Code mit Docstrings und machen ihn etwas klarer:

import random
import time

def generate():
    """
    Generate a list of random integers between 0 and 99, validate it, and return the data.

    Returns:
        list[int]: A validated list of random integers.
    """
    data = [random.randint(0, 99) for _ in range(1, 100000)]
    data = validate(data)
    return data

def validate(data, chance_percent=50):
    """
    Filter the data list based on a random chance.

    Args:
        data (list[int]): The list of integers to validate.
        chance_percent (int, optional): Percentage chance to filter out elements.
            Higher values remove more elements. Defaults to 50.

    Returns:
        list[int]: The filtered list of integers.
    """
    data = [x for x in data if random.randint(1, 100) > chance_percent]
    return data

def search_function(data, targets=[42, 17]):
    """
    Count how many times target values appear in the data list.

    Args:
        data (list[int]): The list of integers to search.
        targets (list[int], optional): Target integers to search for.
            Defaults to [42, 17].

    Returns:
        int: The count of target values found in the data.
    """
    counter = 0
    for value in data:
        if value in targets:
            counter += 1
    return counter

def ascii_renderer(result):
    """
    Print a message character by character with a delay.

    Args:
        result (int): The result count to include in the message.
    """
    message = f"I found the targets {result} times!"
    for char in message:
        time.sleep(0.01)
        print(char, end="")
    print()  # For newline after the message.

def main():
    """
    Orchestrate the program: generate data, search, and render the result.
    """
    data = generate()
    result = search_function(data)
    ascii_renderer(result)

if __name__ == "__main__":
    main()

Warum vor dem Profiling aufräumen?


Profiling mit cProfile

cProfile ist ein in Python eingebauter Profiler, der Funktionsaufrufe und Laufzeiten misst.

cProfile von der Kommandozeile ausführen

Speichere das Skript als profile_example.py und führe im Terminal aus:

python -m cProfile profile_example.py

Dadurch wird dein Skript mit dem Profiler ausgeführt und eine Tabelle mit Profiling-Daten ausgegeben.

Ausgabe interpretieren

Eine typische Ausgabe könnte so aussehen:

         50007 function calls in 1.215 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.005    0.005    1.215    1.215 profile_example.py:42(main)
        1    0.197    0.197    1.210    1.210 profile_example.py:8(generate)
        1    0.167    0.167    1.013    1.013 profile_example.py:15(validate)
    99999    0.846    0.000    0.846    0.000 profile_example.py:16(<listcomp>)
        1    0.001    0.001    0.004    0.004 profile_example.py:22(search_function)
    49996    0.004    0.000    0.004    0.000 profile_example.py:24(<listcomp>)
        1    0.000    0.000    0.000    0.000 profile_example.py:33(ascii_renderer)
       32    0.000    0.000    0.000    0.000 {built-in method time.sleep}

Bedeutung der Spalten:

Ergebnis interpretieren – welche Teile brauchen am längsten?

Schau dir die Zeilen mit den größten cumtime- und/oder tottime-Werten an:

Typische Frage an dieser Stelle:

Welche Funktion oder Code-Stelle sollten wir zuerst optimieren?

Mögliche Antworten:

Wichtige Erkenntnis:

Profiling zeigt dir genau, wo Zeit verbrannt wird. Du musst nicht raten, welche Funktion langsam ist – du kannst es aus der Tabelle ablesen.


Visualisierung mit snakeviz

Texttabellen sind mächtig, aber manchmal ist es leichter, Performance-Daten grafisch zu sehen.

snakeviz kann Profildaten einlesen und als interaktive Visualisierung darstellen.

Installation

pip install snakeviz

Profildaten erzeugen

Anstatt die Statistiken direkt auszugeben, schreiben wir sie in eine Datei:

python -m cProfile -o profile_data.prof profile_example.py

Profil visualisieren

Starte nun snakeviz:

snakeviz profile_data.prof

Es sollte sich ein Browser-Fenster (oder Tab) öffnen mit:

Visualisierung interpretieren

Im Sunburst-Diagramm:

Fragen, die du dir stellen kannst:

Danach kannst du entscheiden, wo sich Optimierungen lohnen:


Fazit

Kurze Zusammenfassung:

Wichtige Punkte: