Leistungsanalyse von Python-Code mit timeit

1. Einleitung

Das timeit-Modul ist ein standardmäßiges Python-Modul, das zur Leistungsanalyse von Python-Code verwendet wird. Es ermöglicht es Entwicklern, die Ausführungszeit von Codeblöcken mit hoher Präzision zu messen, was für die Optimierung von Anwendungen und das Finden von Engpässen entscheidend ist.

2. Grundlegende Funktionalitäten

2.1 Hauptfunktionen

Das Modul bietet zwei Hauptfunktionen:

  1. timeit.timeit() – Misst die Zeit für eine einzelne Ausführung eines Code-Blocks
  2. timeit.repeat() – Führt das Timing mehrmals durch und gibt die Ergebnisse zurück

2.2 Klassen

  • timeit.Timer – Eine Klasse, die die Timing-Funktionalität kapselt und für wiederholte Messungen geeignet ist

3. Verwendung

3.1 Einfache Zeitmessung mit timeit.timeit()

import timeit

# Zeitmessung eines Code-Blocks
execution_time = timeit.timeit('sum([1, 2, 3, 4, 5])', number=100000)
print(f"Ausführungszeit: {execution_time:.6f} Sekunden")

3.2 Zeitmessung mit timeit.repeat()

import timeit

# Mehrfachmessung
times = timeit.repeat('sum([1, 2, 3, 4, 5])', number=100000, repeat=5)
print(f"Zeiten: {times}")
print(f"Minimum: {min(times):.6f} Sekunden")

3.3 Verwendung mit Timer-Klasse

import timeit

# Verwendung der Timer-Klasse
timer = timeit.Timer('sum([1, 2, 3, 4, 5])')
execution_time = timer.timeit(number=100000)
print(f"Ausführungszeit: {execution_time:.6f} Sekunden")

4. Parameter

4.1 timeit.timeit()

timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000)

Parameter:

  • stmt (str): Der auszuführende Code (Standard: 'pass')
  • setup (str): Setup-Code, der vor dem Timing ausgeführt wird (Standard: 'pass')
  • timer (callable): Ein Timer-Objekt, das die Zeit messen soll
  • number (int): Anzahl der Ausführungen (Standard: 1.000.000)

4.2 timeit.repeat()

timeit.repeat(stmt='pass', setup='pass', timer=<default timer>, number=1000000, repeat=3)

Parameter:

  • stmt, setup, timer, number: Siehe timeit.timeit()
  • repeat (int): Anzahl der Wiederholungen (Standard: 3)

5. Fortgeschrittene Verwendung

5.1 Timing von Funktionen

import timeit

def my_function():
    return sum(range(100))

# Timing einer Funktion
execution_time = timeit.timeit(my_function, number=10000)
print(f"Funktion ausgeführt in: {execution_time:.6f} Sekunden")

5.2 Timing mit Setup-Code

import timeit

# Setup-Code wird nur einmal ausgeführt
setup_code = """
import random
data = [random.randint(1, 1000) for _ in range(1000)]
"""

test_code = """
sorted(data)
"""

execution_time = timeit.timeit(test_code, setup=setup_code, number=1000)
print(f"Sortierung ausgeführt in: {execution_time:.6f} Sekunden")

5.3 Timing mit Timer-Klasse für Wiederholungen

import timeit

def test_function():
    return [x**2 for x in range(100)]

# Timer-Objekt für mehrfache Messungen
timer = timeit.Timer(test_function)
times = timer.repeat(repeat=5, number=1000)
print(f"Minimale Zeit: {min(times):.6f}s")
print(f"Durchschnittliche Zeit: {sum(times)/len(times):.6f}s")

6. Spezielle Features

6.1 Verwendung mit globals und locals

import timeit

def example():
    x = 100
    return x * 2

# Verwendung von globals
execution_time = timeit.timeit('example()', globals=globals(), number=10000)
print(f"Zeit mit globals: {execution_time:.6f} Sekunden")

6.2 Timing von Code in String-Format

import timeit

# Timing von Code als String
code = """
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

fibonacci(10)
"""

execution_time = timeit.timeit(code, number=1000)
print(f"Fibonacci berechnet in: {execution_time:.6f} Sekunden")

7. Verwendung von functools.partial für Funktionen mit Parametern

Die Verwendung von functools.partial ist besonders nützlich, wenn man Funktionen mit Parametern in timeit messen möchte. Dies ist notwendig, weil timeit Funktionen ohne Parameter aufrufen kann, aber manchmal Funktionen mit Parametern benötigt.

7.1 Grundlegende Verwendung von functools.partial

import timeit
from functools import partial

def power_function(base, exponent):
    return base ** exponent

# Funktion mit Parametern mit partial
power_of_2 = partial(power_function, base=2)

# Timing der partiell angewendeten Funktion
execution_time = timeit.timeit(power_of_2, number=100000)
print(f"2^exponent ausgeführt in: {execution_time:.6f} Sekunden")

7.2 Timing von Funktionen mit mehreren Parametern

import timeit
from functools import partial

def calculate_area(length, width, unit='m²'):
    return f"{length * width} {unit}"

# Partielle Anwendung mit mehreren Parametern
area_calc = partial(calculate_area, length=10, width=5)

# Timing der Funktion
execution_time = timeit.timeit(area_calc, number=100000)
print(f"Flächenberechnung ausgeführt in: {execution_time:.6f} Sekunden")

7.3 Komplexeres Beispiel mit Datenverarbeitung

import timeit
from functools import partial

def process_data(data, operation, multiplier=1):
    """Verarbeitet Daten mit einer bestimmten Operation"""
    if operation == 'multiply':
        return [x * multiplier for x in data]
    elif operation == 'add':
        return [x + multiplier for x in data]
    return data

# Partielle Funktionen für verschiedene Operationen
multiply_data = partial(process_data, data=[1, 2, 3, 4, 5], operation='multiply', multiplier=2)
add_data = partial(process_data, data=[1, 2, 3, 4, 5], operation='add', multiplier=10)

# Timing der verschiedenen Operationen
multiply_time = timeit.timeit(multiply_data, number=100000)
add_time = timeit.timeit(add_data, number=100000)

print(f"Multiplikation ausgeführt in: {multiply_time:.6f} Sekunden")
print(f"Addition ausgeführt in: {add_time:.6f} Sekunden")

7.4 Timing von Klassenmethoden mit Parametern

import timeit
from functools import partial

class DataProcessor:
    def __init__(self, data):
        self.data = data

    def filter_and_transform(self, threshold, multiplier):
        return [x * multiplier for x in self.data if x > threshold]

# Instanz erstellen
processor = DataProcessor([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

# Partielle Funktion für spezifische Parameter
filtered_transform = partial(processor.filter_and_transform, threshold=5, multiplier=2)

# Timing
execution_time = timeit.timeit(filtered_transform, number=100000)
print(f"Filter- und Transformationsprozess ausgeführt in: {execution_time:.6f} Sekunden")

7.5 Benchmarking mit verschiedenen Parametern

import timeit
from functools import partial

def fibonacci(n, cache={}):
    """Fibonacci-Funktion mit Cache"""
    if n in cache:
        return cache[n]
    if n <= 1:
        return n
    cache[n] = fibonacci(n-1, cache) + fibonacci(n-2, cache)
    return cache[n]

# Partielle Funktionen für verschiedene Fibonacci-Zahlen
fib_10 = partial(fibonacci, n=10)
fib_20 = partial(fibonacci, n=20)
fib_30 = partial(fibonacci, n=30)

# Benchmarking
times = {}
for name, func in [('Fibonacci(10)', fib_10), 
                   ('Fibonacci(20)', fib_20), 
                   ('Fibonacci(30)', fib_30)]:
    time = timeit.timeit(func, number=1000)
    times[name] = time
    print(f"{name}: {time:.6f} Sekunden")

# Ausgabe der Ergebnisse
for name, time in times.items():
    print(f"{name}: {time:.6f} Sekunden")

7.6 Verwendung in Kombination mit anderen Modulen

import timeit
from functools import partial
import math

def complex_calculation(x, y, z):
    """Komplexe Berechnung mit mehreren Parametern"""
    return math.sqrt(x**2 + y**2) * z + math.sin(x) - math.cos(y)

# Partielle Funktion für spezifische Werte
calc = partial(complex_calculation, x=1.5, y=2.5, z=3.0)

# Timing
execution_time = timeit.timeit(calc, number=100000)
print(f"Komplexe Berechnung ausgeführt in: {execution_time:.6f} Sekunden")

# Mehrfachmessung für genauere Ergebnisse
times = timeit.repeat(calc, number=10000, repeat=5)
print(f"Minimale Zeit: {min(times):.6f} Sekunden")
print(f"Durchschnittliche Zeit: {sum(times)/len(times):.6f} Sekunden")

8. Performance-Optimierungen

8.1 Vermeidung von Overhead durch Setup-Code

import timeit
from functools import partial

def my_function(data, multiplier):
    return [x * multiplier for x in data]

# Daten vorbereiten
test_data = list(range(1000))

# Verwende partial um Parameter festzulegen
partial_function = partial(my_function, data=test_data, multiplier=2)

# Timing
execution_time = timeit.timeit(partial_function, number=10000)
print(f"Funktion mit partial ausgeführt in: {execution_time:.6f} Sekunden")

8.2 Vergleich verschiedener Ansätze

import timeit
from functools import partial

def process_list(data, operation, factor):
    if operation == 'multiply':
        return [x * factor for x in data]
    elif operation == 'add':
        return [x + factor for x in data]

# Daten vorbereiten
data = list(range(1000))

# Ansatz 1: String-Code
time1 = timeit.timeit('process_list(data, "multiply", 2)', 
                     globals=globals(), number=1000)

# Ansatz 2: Partial-Funktion
partial_func = partial(process_list, data=data, operation='multiply', factor=2)
time2 = timeit.timeit(partial_func, number=1000)

print(f"String-Code: {time1:.6f}s")
print(f"Partial-Funktion: {time2:.6f}s")
print(f"Partial ist {time1/time2:.2f}x schneller" if time2 < time1 else f"String ist {time2/time1:.2f}x schneller")

9. Best Practices

9.1 Genauigkeit und Wiederholungen mit partial

import timeit
from functools import partial

def benchmark_with_parameters(func, *args, **kwargs):
    """Benchmark-Funktion mit Parametern"""
    partial_func = partial(func, *args, **kwargs)
    times = timeit.repeat(partial_func, number=1000, repeat=5)
    return {
        'times': times,
        'min_time': min(times),
        'mean_time': sum(times) / len(times),
        'best_case': min(times)
    }

# Beispiel
def expensive_calculation(x, y, z):
    return sum(i**2 for i in range(x, y, z))

result = benchmark_with_parameters(expensive_calculation, 1, 100, 2)
print(f"Minimale Zeit: {result['min_time']:.6f}s")
print(f"Durchschnitt: {result['mean_time']:.6f}s")

9.2 Strukturierte Benchmarking-Funktion mit partial

import timeit
from functools import partial

def create_benchmark_suite(func, param_combinations):
    """
    Erstellt eine Benchmark-Suite für verschiedene Parameterkombinationen
    """
    results = {}

    for name, params in param_combinations.items():
        if isinstance(params, dict):
            partial_func = partial(func, **params)
        else:
            partial_func = partial(func, *params)

        times = timeit.repeat(partial_func, number=1000, repeat=3)
        results[name] = {
            'times': times,
            'min_time': min(times),
            'max_time': max(times),
            'avg_time': sum(times) / len(times)
        }

    return results

# Beispielverwendung
def parameterized_function(a, b, c=1):
    return sum(range(a, b, c))

# Parameterkombinationen
params = {
    'small_range': {'a': 1, 'b': 100, 'c': 1},
    'large_range': {'a': 1, 'b': 1000, 'c': 1},
    'step_by_2': {'a': 1, 'b': 100, 'c': 2}
}

# Benchmark durchführen
results = create_benchmark_suite(parameterized_function, params)

for name, result in results.items():
    print(f"{name}:")
    print(f"  Min: {result['min_time']:.6f}s")
    print(f"  Avg: {result['avg_time']:.6f}s")
    print(f"  Max: {result['max_time']:.6f}s")

10. Fehlerbehandlung

10.1 Umgang mit Fehlern bei partial-Funktionen

import timeit
from functools import partial

def risky_function(data, multiplier, divisor=1):
    if divisor == 0:
        raise ValueError("Divisor cannot be zero")
    return [x * multiplier / divisor for x in data]

# Test mit korrekten Parametern
try:
    safe_func = partial(risky_function, data=[1, 2, 3, 4, 5], multiplier=2, divisor=1)
    time = timeit.timeit(safe_func, number=1000)
    print(f"Zeit ohne Fehler: {time:.6f}s")
except Exception as e:
    print(f"Fehler: {e}")

# Test mit fehlerhaften Parametern
try:
    error_func = partial(risky_function, data=[1, 2, 3, 4, 5], multiplier=2, divisor=0)
    time = timeit.timeit(error_func, number=1000)
    print(f"Zeit mit Fehler: {time:.6f}s")
except Exception as e:
    print(f"Fehler bei Timing: {e}")

11. Anwendungsfälle

11.1 Algorithmus-Vergleich mit Parametern

import timeit
from functools import partial

def bubble_sort(arr, reverse=False):
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            if (arr[j] > arr[j+1]) != reverse:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr

def quick_sort(arr, reverse=False):
    if len(arr) <= 1:
        return arr if not reverse else arr[::-1]
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    result = quick_sort(left, reverse) + middle + quick_sort(right, reverse)
    return result if not reverse else result[::-1]

# Daten vorbereiten
data = list(range(1000, 0, -1))

# Partielle Funktionen für verschiedene Sortieroptionen
bubble_asc = partial(bubble_sort, reverse=False)
bubble_desc = partial(bubble_sort, reverse=True)
quick_asc = partial(quick_sort, reverse=False)
quick_desc = partial(quick_sort, reverse=True)

# Vergleich der Algorithmen
times = {}
for name, func in [('Bubble Sort (asc)', bubble_asc),
                   ('Bubble Sort (desc)', bubble_desc),
                   ('Quick Sort (asc)', quick_asc),
                   ('Quick Sort (desc)', quick_desc)]:
    try:
        time = timeit.timeit(lambda: func(data.copy()), number=10)
        times[name] = time
        print(f"{name}: {time:.6f}s")
    except Exception as e:
        print(f"Fehler bei {name}: {e}")

# Ausgabe der Ergebnisse
for name, time in sorted(times.items(), key=lambda x: x[1]):
    print(f"{name}: {time:.6f}s")

11.2 Speicher- und Zeit-Optimierung mit partial

import timeit
from functools import partial

def memory_intensive_operation(data, operation, threshold=0):
    """Speicherintensive Operation mit Parametern"""
    if operation == 'filter':
        return [x for x in data if x > threshold]
    elif operation == 'transform':
        return [x * 2 for x in data if x > threshold]
    return data

# Daten vorbereiten
large_data = list(range(10000))

# Partielle Funktionen für verschiedene Operationen
filter_op = partial(memory_intensive_operation, data=large_data, operation='filter', threshold=5000)
transform_op = partial(memory_intensive_operation, data=large_data, operation='transform', threshold=5000)

# Timing
filter_time = timeit.timeit(filter_op, number=1000)
transform_time = timeit.timeit(transform_op, number=1000)

print(f"Filter-Operation: {filter_time:.6f}s")
print(f"Transform-Operation: {transform_time:.6f}s")

# Mehrfachmessung für genauere Ergebnisse
filter_times = timeit.repeat(filter_op, number=100, repeat=5)
transform_times = timeit.repeat(transform_op, number=100, repeat=5)

print(f"Filter - Min: {min(filter_times):.6f}s, Avg: {sum(filter_times)/len(filter_times):.6f}s")
print(f"Transform - Min: {min(transform_times):.6f}s, Avg: {sum(transform_times)/len(transform_times):.6f}s")

12. Zusammenfassung

Das timeit-Modul ist ein leistungsstarkes Werkzeug für die Leistungsanalyse in Python:

  • Hohe Präzision: Verwendet den besten verfügbaren Timer
  • Wiederholungsmöglichkeiten: Mehrfachmessungen für genaue Ergebnisse
  • Flexible Anpassung: Unterstützung für Setup-Code, verschiedene Wiederholungen
  • Leistungssteigerung: Optimierte Ausführung für Benchmarking
  • Einfache Integration: Einfaches Interface für Entwickler

Die Verwendung von functools.partial ist besonders wichtig, wenn man Funktionen mit Parametern in timeit messen möchte. Dies ermöglicht:

  1. Klare Trennung von Parametern und Zeitmessung
  2. Wiederverwendbare Timing-Funktionen
  3. Effizientere Benchmarking-Prozesse
  4. Bessere Lesbarkeit und Wartbarkeit des Codes

Die Kombination von timeit und functools.partial ist ein mächtiges Werkzeug für Entwickler, die präzise Performance-Messungen durchführen und optimierte Code-Strukturen entwickeln möchten.

Previous