Zum Inhalt springen
aviral gupta

// A4.1 · ca. 30 Min. · Vertiefung

Messen mit timeit

Nach dieser Lektion messen Sie Code mit timeit.timeit und timeit.repeat, geben das Minimum pro Aufruf statt eines verrauschten Werts an und vergleichen zwei Versionen fair: gleiche Eingabe, gleiches Ergebnis, ein Verhältnis.

Lektion 1 von 5 in A4 Leistung und Zahlen

Anfang des Moduls

Danach können Sie

  • Eine Anweisung oder ein Callable mit timeit.timeit und timeit.repeat messen, mit setup, number und globals
  • Erklären, warum eine einzelne Messung täuscht, und das Minimum der Wiederholungen pro Aufruf angeben
  • Zwei Versionen fair vergleichen: Ergebnisse prüfen, gleich messen, das Verhältnis beurteilen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Modul A3: Welche Aussage über Threads stimmt beim normalen CPython-Build mit GIL?

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: work zählt, wie oft es aufgerufen wird. Was gibt das aus?

    import timeit
    
    calls = 0
    
    
    def work():
        global calls
        calls += 1
    
    
    timeit.timeit(work, number=1000)
    first = calls
    times = timeit.repeat(work, number=1000, repeat=5)
    print(first, calls, len(times))
  3. Üben · Aufgabe 3 von 7

    Setzen Sie das Schlüsselwort ein, damit die Liste einmal gebaut wird, außerhalb der gemessenen Anweisung.

    import timeit
    
    setup_runs = []
    timeit.timeit("data.sort()", ____="data = [3, 1, 2]; setup_runs.append(1)", number=100, globals=globals())
    ="data = [3, 1, 2]; setup_runs.append(1)"
  4. Üben · Aufgabe 4 von 7

    timeit.timeit(stmt) mit dem Standardwert für number lieferte 0.84. Wie lange dauert ein Durchlauf von stmt?

  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jedem Teil eines timeit-Aufrufs zu, was er bewirkt.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Das Lambda notiert, ob der Garbage Collector an ist, während es gemessen wird. Was gibt das aus?

    import gc
    import timeit
    
    flags = []
    timeit.timeit(lambda: flags.append(gc.isenabled()), number=3)
    print(flags, gc.isenabled())
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie zwei Funktionen, die den Text "0,1,2,…,999" bauen: eine mit ",".join, eine mit += in einer Schleife. Geben Sie zuerst aus, ob beide denselben Text liefern. Messen Sie dann jede mit timeit.repeat (number=200, repeat=5) und geben Sie das Minimum pro Aufruf in Mikrosekunden und das Verhältnis aus. Starten Sie es dreimal: Welche Werte ändern sich, welche Schlussfolgerung bleibt?

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Zwei Versionen fair gemessen

Zwei Funktionen addieren die Zahlen unter n: eine mit einer Schleife, eine mit einer Formel. Das Programm prüft zuerst, dass beide übereinstimmen, misst dann jede mit timeit.repeat und behält die schnellste Messung pro Aufruf. Die Zeiten selbst ändern sich bei jedem Lauf und auf jedem Rechner, darum gibt das Programm nur aus, was gleich bleibt: ob die Ergebnisse übereinstimmen und ob das Verhältnis eine Schwelle überschreitet. Geben Sie slow und fast zusätzlich aus, um die schwankenden Rohwerte zu sehen.

main.py

import timeit
from collections.abc import Callable


def total_loop(n: int) -> int:
    total = 0
    for i in range(n):
        total += i
    return total


def total_formula(n: int) -> int:
    return n * (n - 1) // 2


def best_per_call(func: Callable[[], object], number: int, repeat: int = 5) -> float:
    """The fastest of `repeat` timings, divided by the calls in each timing."""
    times = timeit.repeat(func, number=number, repeat=repeat)
    return min(times) / number


n = 100_000
# 1. Same input, same answer: otherwise the timing compares different work.
print("same result:", total_loop(n) == total_formula(n))

# 2. Time both the same way. The seconds differ on every run.
slow = best_per_call(lambda: total_loop(n), number=20)
fast = best_per_call(lambda: total_formula(n), number=20)

# 3. Print what does not vary: the ratio against a threshold.
print("formula at least 100x faster:", slow / fast >= 100)
print("faster version:", "formula" if fast < slow else "loop")

Ausführen mit

python main.py

Ausgabe

same result: True
formula at least 100x faster: True
faster version: formula
  • Die Prüfung auf Richtigkeit kommt zuerst: Zwei Versionen, die sich widersprechen, erledigen verschiedene Arbeit.
  • best_per_call teilt durch number, die Werte gelten also pro Aufruf, egal welche Schleifenzahl Sie wählen.
  • Das Verhältnis liegt in den Tausendern, die Schwelle 100 hält also auf jedem Rechner; die Sekunden nicht.
  • Jedes Lambda reicht n durch, ohne Vorbereitung mitzumessen: timeit ruft das Callable auf.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Die beste Zeit pro Aufruf

Vervollständigen Sie best_per_call(func, number=1000, repeat=5). Die Funktion ruft timeit.repeat mit der Funktion, number und repeat auf (schreiben Sie import timeit und timeit.repeat, damit die Tests es ersetzen können) und gibt das Minimum der Messungen geteilt durch number zurück. Die Tests zählen Aufrufe und geben erfundene Messwerte vor; sie hängen nie davon ab, wie schnell dieser Rechner ist.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    timeit.repeat(func, number=number, repeat=repeat) liefert eine Liste mit einer Summe pro Messung.

  2. Hinweis 2

    Jede Summe umfasst number Aufrufe, die Zeit pro Aufruf ist also min(times) / number.

  3. Hinweis 3

    Der ganze Rumpf passt in zwei Zeilen: times = timeit.repeat(...), dann return min(times) / number.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import timeit
from collections.abc import Callable


def best_per_call(func: Callable[[], object], number: int = 1000, repeat: int = 5) -> float:
    times = timeit.repeat(func, number=number, repeat=repeat)
    return min(times) / number
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import timeit
from collections.abc import Callable


def best_per_call(func: Callable[[], object], number: int = 1000, repeat: int = 5) -> float:
    # One timing is not enough: take `repeat` timings of `number` calls each,
    # and return the fastest one divided by `number`.
    return timeit.timeit(func, number=number)

test_main.py

import math
import timeit
from unittest import mock

from main import best_per_call


def test_calls():
    """number=10 und repeat=3 rufen die Funktion 30-mal auf"""
    calls = []
    best_per_call(lambda: calls.append(1), number=10, repeat=3)
    assert len(calls) == 30, f"die Funktion wurde {len(calls)}-mal aufgerufen, erwartet 30 (10 Aufrufe in jeder von 3 Messungen)"


def test_minimum_per_call():
    """Messungen von 0,5, 0,2 und 0,4 s für 100 Aufrufe ergeben 0,002 s pro Aufruf"""
    with mock.patch("timeit.repeat", return_value=[0.5, 0.2, 0.4]) as fake:
        got = best_per_call(lambda: None, number=100, repeat=3)
    assert fake.called, "best_per_call hat timeit.repeat nicht aufgerufen"
    assert math.isclose(got, 0.002), f"Ergebnis {got!r}, erwartet 0.002: das Minimum 0.2 geteilt durch number=100"


def test_defaults():
    """Ohne Argumente: 5 Messungen mit je 1000 Aufrufen"""
    calls = []
    best_per_call(lambda: calls.append(1))
    assert len(calls) == 5000, f"die Funktion wurde {len(calls)}-mal aufgerufen, erwartet 5000"


def test_returns_float():
    """Ein echter Lauf liefert einen positiven float"""
    got = best_per_call(lambda: sum(range(10)), number=50)
    assert isinstance(got, float) and got > 0, f"Ergebnis {got!r}, erwartet ein positiver float"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 2

Ein fairer Vergleich

Vervollständigen Sie compare(slow, fast, arg, number=200). Prüfen Sie zuerst, ob slow(arg) und fast(arg) gleich sind, und lösen Sie sonst ValueError aus, bevor Sie irgendetwas messen. Messen Sie dann lambda: slow(arg) und lambda: fast(arg) mit timeit.repeat (number=number, repeat=5) und geben Sie das Minimum der langsamen Messungen geteilt durch das Minimum der schnellen zurück. 3.0 heißt: Die schnelle Version ist dreimal schneller.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Beginnen Sie mit if slow(arg) != fast(arg): raise ValueError(...). Erst danach wird gemessen.

  2. Hinweis 2

    timeit.repeat(lambda: slow(arg), number=number, repeat=5) liefert fünf Summen; behalten Sie deren min().

  3. Hinweis 3

    Das Verhältnis ist langsam durch schnell: Eine größere Zahl bedeutet einen größeren Gewinn der schnellen Version.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import timeit
from collections.abc import Callable


def compare(slow: Callable[[int], object], fast: Callable[[int], object], arg: int, number: int = 200) -> float:
    if slow(arg) != fast(arg):
        raise ValueError("the two versions give different results")
    slow_best = min(timeit.repeat(lambda: slow(arg), number=number, repeat=5))
    fast_best = min(timeit.repeat(lambda: fast(arg), number=number, repeat=5))
    return slow_best / fast_best
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import timeit
from collections.abc import Callable


def compare(slow: Callable[[int], object], fast: Callable[[int], object], arg: int, number: int = 200) -> float:
    # 1. Raise ValueError if slow(arg) and fast(arg) differ.
    # 2. Time both with timeit.repeat and return min(slow) / min(fast).
    slow_time = timeit.timeit(lambda: slow(arg), number=number)
    fast_time = timeit.timeit(lambda: fast(arg), number=number)
    return fast_time / slow_time

test_main.py

import math
from unittest import mock

from main import compare

who = []


def slow(n):
    who.append("slow")
    return n * 2


def fast(n):
    who.append("fast")
    return n + n


def wrong(n):
    who.append("wrong")
    return n


def fake_repeat(stmt, number=1000000, repeat=5, **kwargs):
    """Ruft stmt einmal auf und liefert erfundene Messwerte für die Version, die lief."""
    stmt()
    return [0.9, 0.6, 0.8] if who[-1] == "slow" else [0.3, 0.2, 0.25]


def test_ratio():
    """Bestzeiten von 0,6 s und 0,2 s ergeben das Verhältnis 3,0"""
    with mock.patch("timeit.repeat", side_effect=fake_repeat):
        got = compare(slow, fast, 21)
    assert math.isclose(got, 3.0), f"Ergebnis {got!r}, erwartet 3.0: Minimum der langsamen / Minimum der schnellen Messungen"


def test_different_results():
    """Versionen mit verschiedenen Ergebnissen lösen ValueError aus"""
    try:
        compare(slow, wrong, 21)
    except ValueError:
        return
    raise AssertionError("compare(slow, wrong, 21) hat keinen ValueError ausgelöst, obwohl 42 != 21")


def test_checks_before_timing():
    """Verschiedene Ergebnisse fallen auf, bevor irgendetwas gemessen wird"""
    with mock.patch("timeit.repeat", side_effect=fake_repeat) as fake:
        try:
            compare(slow, wrong, 21)
        except ValueError:
            pass
    assert fake.call_count == 0, f"timeit.repeat wurde vor der Prüfung {fake.call_count}-mal aufgerufen: vergleichen Sie zuerst die Ergebnisse"


def test_real_run():
    """Ein echter Lauf liefert einen positiven float"""
    got = compare(slow, fast, 5, number=20)
    assert isinstance(got, float) and got > 0, f"Ergebnis {got!r}, erwartet ein positiver float"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Eine String-Anweisung sieht Ihre Funktion nicht

import timeit


def square(n):
    return n * n


print(timeit.timeit("square(3)", number=10))

Was Python ausgibt

NameError: name 'square' is not defined

Warum, und die Lösung

Eine String-Anweisung läuft im eigenen Namensraum von timeit, in dem Ihre Funktionen fehlen. Übergeben Sie globals=globals(), importieren Sie die Funktion in setup ("from __main__ import square") oder übergeben Sie ein Callable: timeit.timeit(lambda: square(3), number=10).

Das Ergebnis statt der Funktion übergeben

import timeit


def work():
    sum(range(100))


print(timeit.timeit(work(), number=10))

Was Python ausgibt

ValueError: stmt is neither a string nor callable

Warum, und die Lösung

work() ruft die Funktion einmal auf, bevor timeit beginnt, und übergibt ihr Ergebnis, None. Übergeben Sie die Funktion selbst, timeit.timeit(work, number=10), oder ein Lambda, wenn sie Argumente braucht.

Die Liste von repeat wie eine Zahl behandeln

import timeit

times = timeit.repeat("sum(range(100))", number=1000, repeat=5)
print(times / 1000)

Was Python ausgibt

TypeError: unsupported operand type(s) for /: 'list' and 'int'

Warum, und die Lösung

repeat liefert eine Liste mit einer Summe pro Messung. Wählen Sie zuerst einen Wert, das Minimum, und teilen Sie dann durch number: min(times) / 1000 ist die beste Zeit pro Aufruf.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

timeit führt Ihren Code viele Male aus

timeit.timeit(stmt, setup, number=1_000_000, globals=None) führt setup einmal aus, dann stmt number-mal, und gibt die Gesamtzeit in Sekunden als float zurück. stmt und setup dürfen Strings oder Callables ohne Argumente sein. Ein String läuft im eigenen Namensraum von timeit; mit globals=globals() sieht er Ihre Funktionen. Teilen Sie durch number, um die Zeit pro Aufruf zu erhalten: 0,84 s für eine Million Aufrufe sind 0,84 µs pro Aufruf. Die Kommandozeile, python -m timeit, wählt number selbst, die Funktion nicht.

Warum eine einzelne Messung lügt

Andere Prozesse, das Betriebssystem und Caches bremsen manche Durchläufe, aber keiner wird schneller, als der Code erlaubt. timeit.repeat(stmt, number=…, repeat=5) misst mehrmals und liefert die Messungen als Liste. Die Dokumentation empfiehlt das Minimum: Höhere Werte stammen von Störungen, nicht von einem langsameren Python. Geben Sie min(times) / number an. timeit schaltet außerdem den Garbage Collector beim Messen ab; Code, der viele Objekte erzeugt, kann im echten Programm langsamer sein.

Fair vergleichen, nie eine Zeit testen

Prüfen Sie vor dem Messen, dass beide Versionen für dieselbe Eingabe dasselbe Ergebnis liefern: Eine schnelle falsche Antwort ist wertlos. Messen Sie beide mit gleichem number und repeat, bauen Sie Daten in setup auf und beurteilen Sie das Verhältnis, etwa „mindestens 10-mal schneller“. Absolute Zeiten ändern sich je Rechner und Lauf, darum darf ein Test nie eine davon prüfen. Tests zählen Aufrufe oder prüfen Ergebnisse; mehr als ein Verhältnis mit großem Abstand sollte eine Messung nicht versprechen.

Quellen

Zuletzt geprüft am 29. September 2026