Zum Inhalt springen
aviral gupta

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

Was die Wahl der Datenstruktur kostet

Nach dieser Lektion wählen Sie set oder dict für Enthaltenseins-Tests und Lookups und eine deque für Arbeit am linken Ende und können begründen, warum: Arbeit zählen, dann das Verhältnis mit timeit messen.

Lektion 3 von 5 in A4 Leistung und Zahlen

Danach können Sie

  • Für Enthaltenseins-Tests und Lookups set oder dict statt einer Liste nutzen und erklären, warum
  • Eine deque zum Einfügen und Entfernen am linken Ende nutzen statt list.pop(0) und insert(0, x)
  • Einen Unterschied zeigen: Operationen zählen und Zeitverhältnisse bei wachsender Eingabe messen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus der cProfile-Lektion: find_price durchsuchte für jede Bestellung eine Liste von 2000 Paaren. Was zeigte das Profil vor der Korrektur?

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: Key zählt, wie oft == auf ihm aufgerufen wird. Wie viele Vergleiche braucht ein Lookup des letzten Schlüssels in der Liste und in der Menge?

    class Key:
        comparisons = 0
    
        def __init__(self, n):
            self.n = n
    
        def __eq__(self, other):
            Key.comparisons += 1
            return self.n == other.n
    
        def __hash__(self):
            return hash(self.n)
    
    
    keys = [Key(i) for i in range(1000)]
    key_set = set(keys)
    
    Key.comparisons = 0
    Key(999) in keys
    in_list = Key.comparisons
    
    Key.comparisons = 0
    Key(999) in key_set
    print(in_list, Key.comparisons)
  3. Üben · Aufgabe 3 von 7

    Setzen Sie die Methode ein, die das erste Element in konstanter Zeit aus der Warteschlange nimmt.

    first = queue.____()
    first = queue.()
  4. Üben · Aufgabe 4 von 7

    items ist eine Liste mit einer Million Elementen. Welche Operation braucht Zeit, die mit der Länge wächst?

  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jeder Operation ihre Kosten zu.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Jemand hat einen Lookup „optimiert“, indem er das Vokabular in eine Menge verwandelt. Key zählt jetzt Aufrufe von __hash__. Was gibt das aus?

    class Key:
        hashes = 0
    
        def __init__(self, n):
            self.n = n
    
        def __eq__(self, other):
            return self.n == other.n
    
        def __hash__(self):
            Key.hashes += 1
            return hash(self.n)
    
    
    words = [Key(i) for i in range(10)]
    vocabulary = [Key(i) for i in range(100)]
    found = [w for w in words if w in set(vocabulary)]
    print(Key.hashes)
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schlagen Sie für n = 1.000, 10.000 und 100.000 einen fehlenden Wert in einer Liste und in einer Menge aus range(n) nach. Zählen Sie zuerst die ==-Aufrufe mit einer Key-Klasse wie oben, messen Sie dann beide Lookups mit timeit.repeat, die Daten außerhalb des gemessenen Lambdas gebaut. Geben Sie eine Zeile pro n aus. Was passiert mit jeder Zahl und jeder Zeit, wenn n sich verzehnfacht?

    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 Entscheidungen zählen und messen

Teil 1 zählt für drei Größen die ==-Aufrufe für einen Lookup des letzten Schlüssels in einer Liste und in einer Menge: exakte Zahlen, auf jedem Rechner gleich. Teil 2 misst für 100.000 Elemente Liste gegen Menge beim Enthaltensein und list.pop(0) gegen deque.popleft(). Die Sekunden ändern sich bei jedem Lauf, darum gibt das Programm nur aus, ob jedes Verhältnis eine großzügige Schwelle überschreitet. Geben Sie t_list und t_set zusätzlich aus, um die Rohwerte zu sehen.

main.py

import timeit
from collections import deque


class Key:
    """An int wrapper that counts how often Python compares it."""

    comparisons = 0

    def __init__(self, n: int) -> None:
        self.n = n

    def __eq__(self, other: object) -> bool:
        Key.comparisons += 1
        return isinstance(other, Key) and self.n == other.n

    def __hash__(self) -> int:
        return hash(self.n)


# 1. Count the work: == calls for one lookup of the last key.
for n in (1000, 2000, 4000):
    keys = [Key(i) for i in range(n)]
    key_set = set(keys)
    Key.comparisons = 0
    Key(n - 1) in keys
    in_list = Key.comparisons
    Key.comparisons = 0
    Key(n - 1) in key_set
    print(f"n={n}: list compares {in_list}, set compares {Key.comparisons}")

# 2. Time it. The seconds vary; ratios this large do not.
n = 100_000
data = list(range(n))
data_set = set(data)
t_list = min(timeit.repeat(lambda: n - 1 in data, number=20, repeat=5))
t_set = min(timeit.repeat(lambda: n - 1 in data_set, number=20, repeat=5))
print("list 'in' at least 100x slower than set 'in':", t_list / t_set >= 100)

items = list(range(n))
queue = deque(items)


def list_left() -> None:
    items.pop(0)  # shifts every other item one place
    items.append(0)


def deque_left() -> None:
    queue.popleft()  # constant time at either end
    queue.append(0)


t_pop = min(timeit.repeat(list_left, number=200, repeat=5))
t_popleft = min(timeit.repeat(deque_left, number=200, repeat=5))
print("list.pop(0) at least 10x slower than deque.popleft():", t_pop / t_popleft >= 10)

Ausführen mit

python main.py

Ausgabe

n=1000: list compares 1000, set compares 1
n=2000: list compares 2000, set compares 1
n=4000: list compares 4000, set compares 1
list 'in' at least 100x slower than set 'in': True
list.pop(0) at least 10x slower than deque.popleft(): True
  • Doppeltes n verdoppelt die Vergleiche der Liste; die Menge braucht einen, egal wie groß.
  • Der eine Vergleich der Menge bestätigt, dass der Schlüssel im Slot wirklich gleich ist: gleiche Hashes allein genügen nicht.
  • Die gemessenen Verhältnisse liegen in den Tausendern und Hundertern, weit über den ausgegebenen Schwellen.
  • Jede gemessene Funktion hält die Liste gleich lang, sodass jeder Lauf dieselbe Arbeit misst.
Ä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

Duplikate ohne Durchsuchen

find_duplicates liefert die Elemente, die mehr als einmal vorkommen, jedes einmal, in der Reihenfolge ihrer ersten Wiederholung. Die Funktion arbeitet korrekt, merkt sich Gesehenes aber in Listen, sodass jede Prüfung sie durchsucht. Lassen Sie das Ergebnis exakt gleich und machen Sie beide Prüfungen zu Hash-Lookups. Ein Test zählt die ==-Vergleiche für 401 Schlüssel.

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

    seen und reported werden immer nur gefragt: „Enthältst du dieses Element?“ Genau dafür gibt es Mengen.

  2. Hinweis 2

    Beginnen Sie beide mit set() und nutzen Sie .add() statt .append().

  3. Hinweis 3

    duplicates selbst bleibt eine Liste, weil die Reihenfolge zum Ergebnis gehört.

Eine Lösung zeigen

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

from collections.abc import Hashable


def find_duplicates(items: list[Hashable]) -> list[Hashable]:
    """Items that occur more than once, each listed once, in order of their second occurrence."""
    seen: set[Hashable] = set()
    reported: set[Hashable] = set()
    duplicates = []
    for item in items:
        if item in seen:
            if item not in reported:
                reported.add(item)
                duplicates.append(item)
        else:
            seen.add(item)
    return duplicates
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

from collections.abc import Hashable


def find_duplicates(items: list[Hashable]) -> list[Hashable]:
    """Items that occur more than once, each listed once, in order of their second occurrence."""
    seen = []
    reported = []
    duplicates = []
    for item in items:
        if item in seen:
            if item not in reported:
                reported.append(item)
                duplicates.append(item)
        else:
            seen.append(item)
    return duplicates

test_main.py

from main import find_duplicates


class Key:
    """Eine int-Hülle, die zählt, wie oft Python sie vergleicht."""

    comparisons = 0

    def __init__(self, n):
        self.n = n

    def __eq__(self, other):
        Key.comparisons += 1
        return isinstance(other, Key) and self.n == other.n

    def __hash__(self):
        return hash(self.n)

    def __repr__(self):
        return f"Key({self.n})"


def test_duplicates():
    """3 und 1 wiederholen sich: [3, 1], jedes einmal"""
    got = find_duplicates([3, 1, 3, 2, 1, 3])
    assert got == [3, 1], f"Ergebnis {got!r}, erwartet [3, 1]"


def test_no_duplicates():
    """Verschiedene Elemente ergeben eine leere Liste"""
    got = find_duplicates(["a", "b", "c"])
    assert got == [], f"Ergebnis {got!r}, erwartet []"


def test_comparisons():
    """400 verschiedene Schlüssel brauchen fast keine ==-Vergleiche"""
    items = [Key(i) for i in range(400)] + [Key(7)]
    Key.comparisons = 0
    got = find_duplicates(items)
    assert [k.n for k in got] == [7], f"Ergebnis {got!r}, erwartet [Key(7)]"
    assert Key.comparisons < 50, f"find_duplicates machte {Key.comparisons} ==-Vergleiche; eine Liste wird Element für Element durchsucht, eine Menge nicht"

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 begrenzter Verlauf

History behält die letzten size Ereignisse, das älteste zuerst. Der Startcode entfernt das älteste Ereignis mit list.pop(0), was alle anderen verschiebt. Speichern Sie die Ereignisse stattdessen in einer collections.deque mit maxlen=size, sodass eine volle deque ihr ältestes Ereignis selbst in konstanter Zeit verwirft. Behalten Sie den Attributnamen events bei, und oldest() und recent() müssen weiter funktionieren; recent() liefert eine neue Liste.

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

    from collections import deque, dann self.events: deque[str] = deque(maxlen=size).

  2. Hinweis 2

    Mit maxlen verwirft append auf einer vollen deque ein Element am anderen Ende: add braucht kein if.

  3. Hinweis 3

    events[0] ist weiterhin das älteste Ereignis, und das ist schnell: Indizieren einer deque geht an beiden Enden flott.

Eine Lösung zeigen

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

from collections import deque


class History:
    """The last `size` events, oldest first."""

    def __init__(self, size: int) -> None:
        self.size = size
        self.events: deque[str] = deque(maxlen=size)

    def add(self, event: str) -> None:
        self.events.append(event)  # a full deque drops its oldest item itself

    def oldest(self) -> str:
        return self.events[0]

    def recent(self) -> list[str]:
        return list(self.events)
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

class History:
    """The last `size` events, oldest first."""

    def __init__(self, size: int) -> None:
        self.size = size
        self.events: list[str] = []

    def add(self, event: str) -> None:
        self.events.append(event)
        if len(self.events) > self.size:
            self.events.pop(0)  # shifts every other event

    def oldest(self) -> str:
        return self.events[0]

    def recent(self) -> list[str]:
        return list(self.events)

test_main.py

from collections import deque

from main import History


def test_keeps_last_three():
    """Mit size 3 bleiben nach a bis e nur c, d und e"""
    h = History(3)
    for event in "abcde":
        h.add(event)
    assert h.recent() == ["c", "d", "e"], f"recent() lieferte {h.recent()!r}, erwartet ['c', 'd', 'e']"


def test_oldest():
    """oldest() ist das erste noch gespeicherte Ereignis"""
    h = History(2)
    for event in ["login", "view", "logout"]:
        h.add(event)
    assert h.oldest() == "view", f"oldest() lieferte {h.oldest()!r}, erwartet 'view'"


def test_uses_bounded_deque():
    """events ist eine deque mit maxlen gleich size"""
    h = History(4)
    ok = isinstance(h.events, deque) and h.events.maxlen == 4
    assert ok, f"events ist {h.events!r}; erwartet deque(maxlen=4), die das älteste Element in konstanter Zeit verwirft"


def test_recent_is_a_copy():
    """recent() liefert eine neue Liste, nicht den internen Speicher"""
    h = History(2)
    h.add("a")
    h.recent().append("x")
    assert h.recent() == ["a"], f"eine Änderung an der Liste von recent() hat den Verlauf geändert: {h.recent()!r}"

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

Listen in eine Menge stecken

rows = [[1, 2], [3, 4], [1, 2]]
unique = set(rows)
print(len(unique))

Was Python ausgibt

TypeError: cannot use 'list' as a set element (unhashable type: 'list')

Warum, und die Lösung

Elemente einer Menge und Schlüssel eines dict müssen hashbar sein, und eine Liste ist es nicht, weil sie sich ändern kann. Wandeln Sie jede Zeile erst in ein Tupel um: set(tuple(row) for row in rows) ergibt 2 verschiedene Zeilen.

{} für eine leere Menge schreiben

seen = {}
seen.add("ada")

Was Python ausgibt

AttributeError: 'dict' object has no attribute 'add'

Warum, und die Lösung

{} ist ein leeres dict, keine leere Menge, weil dicts die geschweiften Klammern zuerst hatten. Schreiben Sie seen = set() für eine leere Menge; {"ada"} mit mindestens einem Element ist eine Menge.

deque.pop einen Index geben

from collections import deque

queue = deque(["a", "b", "c"])
first = queue.pop(0)

Was Python ausgibt

TypeError: deque.pop() takes no arguments (1 given)

Warum, und die Lösung

deque.pop() entfernt immer rechts und nimmt keinen Index. Für das erste Element rufen Sie queue.popleft() auf, das list.pop(0) ersetzt.

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

Durchsuchen oder hashen

Eine Liste ist ein Array von Referenzen, x in a_list vergleicht x also mit einem Element nach dem anderen, bis eines passt, bei einem fehlenden Element mit allen. Doppelt so viele Elemente, doppelte Arbeit. set und dict sind Hashtabellen: Python berechnet hash(x), springt in einen Slot und vergleicht meist einmal, egal wie groß. Der Preis: Elemente und Schlüssel müssen hashbar sein, und das Bauen der Menge kostet einen Durchlauf. Einmal bauen, oft nachschlagen; für einen einzelnen Lookup reicht die Liste.

Das linke Ende einer Liste

append und pop() am rechten Ende einer Liste sind schnell. pop(0) und insert(0, x) nicht: Jedes andere Element muss um eine Stelle rücken, die Kosten wachsen mit der Länge. collections.deque fügt an beiden Enden in etwa konstanter Zeit ein und entfernt dort: append, appendleft, pop, popleft. deque(maxlen=n) behält nur die neuesten n Elemente und verwirft das älteste selbst. Der Preis: Indizieren ist an den Enden schnell, in der Mitte aber langsam, wo eine Liste schnell ist.

Erst zählen, dann messen

Operationen zu zählen ergibt auf jedem Rechner dasselbe: Eine Klasse, deren __eq__ einen Zähler erhöht, zeigt 1000 Vergleiche für einen Listen-Lookup und 1 für eine Menge. Die Messung bestätigt dann, dass es zählt. Messen Sie den fairen schlechtesten Fall, etwa das letzte oder ein fehlendes Element, bauen Sie die Daten außerhalb des gemessenen Codes und probieren Sie zwei oder drei Größen. Verdoppelt sich mit n die Zeit, wachsen die Kosten mit n. Nennen Sie ein Verhältnis mit Schwelle, nie rohe Sekunden.

Quellen

Zuletzt geprüft am 29. September 2026