Aufwärmen · Aufgabe 1 von 7
// 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.
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
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)Ü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.()Üben · Aufgabe 4 von 7
items ist eine Liste mit einer Million Elementen. Welche Operation braucht Zeit, die mit der Länge wächst?
Üben · Aufgabe 5 von 7
Ordnen Sie jeder Operation ihre Kosten zu.
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)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.pyAusgabe
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
Hinweis 1
seen und reported werden immer nur gefragt: „Enthältst du dieses Element?“ Genau dafür gibt es Mengen.
Hinweis 2
Beginnen Sie beide mit set() und nutzen Sie .add() statt .append().
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.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
Hinweis 1
from collections import deque, dann self.events: deque[str] = deque(maxlen=size).
Hinweis 2
Mit maxlen verwirft append auf einer vollen deque ein Element am anderen Ende: add braucht kein if.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHä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.