Datentypen und Collections (Referenzen, Liste, Dict, Set, Zahlen)

Track Python · PCAP-Fundament · ca. 55 Min.

Worum es geht

In JS/TS denkst du bei const a = [1, 2]; const b = a; automatisch richtig: Arrays und Objekte sind Referenzen. In Python gilt das für alles: Eine Variable ist nur ein Name, der auf ein Objekt zeigt. Dazu kommt die strenge Trennung in veränderbare (mutable) und unveränderbare (immutable) Typen. Daraus entstehen die häufigsten Python-Fehler (geteilte Listen, flache Kopien, Float-Rundung). Nach der Lektion kannst du Aliasing und Kopien vorhersagen, Dicts und Sets gezielt einsetzen und Float-Fallen umgehen.

Ablauf: erst die Übersetzung JS/TS nach Python, dann fünf kleine Schritte mit lauffähigem Code, dann fünf Übungen.

Von JS/TS her gedacht

Thema JS/TS Python
Zahl number (immer Float), BigInt extra int (beliebig groß) und float getrennt
Array Array list (veränderbar), tuple (unveränderbar)
Objekt/Map Object / Map dict (Einfügereihenfolge, Schlüssel müssen hashbar sein)
Set Set set, dazu Operatoren \|, &, -, ^
Kopie [...a], structuredClone(a) a.copy(), a[:], list(a) (flach), copy.deepcopy(a) (tief)
Fehlender Schlüssel obj.k ergibt undefined d["k"] wirft KeyError, d.get("k") ergibt None
Leeres Objekt/Set {} Objekt, new Set() {} ist ein leeres dict, leeres Set nur mit set()
true + true 2 2 (bool ist ein int)

Dieselbe Idee in beiden Sprachen, der Spread als flache Kopie:

// TypeScript: Spread kopiert nur die oberste Ebene
const a = { name: "x", tags: ["a"] };
const b = { ...a };
b.tags.push("b");
console.log(a.tags); // ["a", "b"]

Auch hier steht in a jetzt ['a', 'b']. Genau wie in JS.

Konzept in kleinen Schritten

Schritt 1: Namen zeigen auf Objekte (Referenz, Aliasing)

b = a kopiert nichts. Beide Namen zeigen auf dasselbe Objekt (Referenz, reference). Mit is prüfst du Identität, mit == Gleichheit des Inhalts.

Die bekannte Alias-Falle (alias trap): [[0] * 3] * 3 baut drei Verweise auf dieselbe innere Liste.

Schritt 2: veränderbar oder nicht (mutable vs immutable)

Unveränderbar sind int, float, bool, str, tuple, bytes, frozenset, None. Veränderbar sind list, dict, set, bytearray. Aber Achtung: Ein Tupel ist nur flach unveränderbar. Es enthält Verweise, und diese Verweise können auf veränderbare Objekte zeigen.

Daraus folgt auch hashbar (hashable): Dict-Schlüssel und Set-Elemente müssen unveränderbar sein. int, str, tuple gehen, list, dict nicht.

Beachte: (1) ist nur die Zahl 1. Ein Tupel mit einem Element braucht das Komma: (1,).

Schritt 3: Kopien (flach und tief)

  • Flache Kopie (shallow copy): a.copy(), a[:], list(a), copy.copy(a). Die äußere Liste ist neu, die inneren Objekte werden weiter geteilt.
  • Tiefe Kopie (deep copy): copy.deepcopy(a) kopiert verschachtelte Objekte mit.

Schritt 4: Liste, Dict und Set im Einsatz

Liste

  • l.sort() sortiert direkt und liefert None. sorted(l) liefert eine neue Liste.
  • Slicing mit zu großem Ende wirft keinen Fehler, ein einzelner Index außerhalb wirft IndexError.

Dict

  • d["k"] wirft KeyError bei fehlendem Schlüssel, d.get("k", default) nicht.
  • keys(), values(), items() sind Ansichten (views), die sich mit dem Dict ändern.
  • Das Dict während des Durchlaufens zu verändern wirft RuntimeError.

Set: eindeutig, ungeordnet, Mengenoperationen | (Vereinigung), & (Schnitt), - (Differenz), ^ (symmetrische Differenz).

Eine typische Kombination: Ein dict zählt, ein set vergleicht.

Schritt 5: Zahlen und Float-Fallen

  • int ist beliebig groß, float hat die übliche Fließkommagenauigkeit (IEEE 754, wie number in JS).
  • bool ist ein int: True + True ist 2.
  • / liefert immer einen float, // rundet nach unten (auch bei negativen Zahlen).
  • Schreibweisen: 1_000_000, 0b101, 0o17, 0xFF, 1e3.

Die Float-Falle bei Geld: 0.29 * 100 ist nicht 29.

int() schneidet ab, round() rundet. Wer Geldbeträge als Float speichert, sollte in ganzen Cent rechnen oder ein Rundungswerkzeug wie decimal nutzen (steht nicht in der Quelle, bitte prüfen, ob es in der PCAP drankommt).

Falle: die Prüfungs- und Praxisklassiker

  1. b = a kopiert nicht. Änderungen über b sieht a.
  2. [[0] * 3] * 3: drei Verweise auf dieselbe innere Liste. Richtig: [[0] * 3 for _ in range(3)].
  3. Flache Kopie bei verschachtelten Daten: copy(), [:], list(), dict() teilen die inneren Objekte. Dann copy.deepcopy.
  4. l.sort() liefert None: l = l.sort() zerstört die Variable.
  5. {} ist ein Dict, kein Set. Leeres Set: set().
  6. (1) ist kein Tupel, (1,) schon.
  7. Float-Vergleich mit == und int() statt round() bei berechneten Beträgen.
  8. Dict beim Durchlaufen ändern: RuntimeError.

Übungen

Übung 1: Aliasing und Kopie vorhersagen

Der Code steht fest, du musst nur das Ergebnis vorhersagen:

a = [1, 2]
b = a
b.append(3)
c = a[:]
c.append(4)

gitter = [[0] * 2] * 2
gitter[0][0] = 7

Trage das Tupel (a, c, gitter) ein.

Frage dich bei jeder Zeile: Entsteht hier ein neues Objekt, oder kommt nur ein zweiter Name dazu? Und bei gitter: Wie viele innere Listen gibt es wirklich?

antwort = ([1, 2, 3], [1, 2, 3, 4], [[7, 0], [7, 0]])
antwort

Übung 2: Tupel mit Liste (Multiple Choice)

t = ([1], 2)
t[0] += [5]

Was passiert? Gib den Buchstaben als String zurück.

  • A: TypeError, und t bleibt unverändert ([1], 2).
  • B: Kein Fehler, t ist danach ([1, 5], 2).
  • C: Kein Fehler, t bleibt ([1], 2).
  • D: TypeError, und t ist trotzdem ([1, 5], 2).

+= auf einer Liste macht zwei Dinge hintereinander: erst wird die Liste erweitert, dann wird das Ergebnis wieder an die Stelle t[0] zugewiesen. Welcher der beiden Teile ist bei einem Tupel erlaubt, und welcher nicht?

antwort = "D"
antwort

Übung 3: Kopieren statt verändern

Die Funktion mit_port soll eine neue Konfiguration liefern, in der server["port"] auf den neuen Wert gesetzt ist. Das Original darf sich nicht ändern, und die neue Konfiguration darf auch keine inneren Objekte (zum Beispiel die tags-Liste) mit dem Original teilen. Fülle die Lücken.

Die Konfiguration ist verschachtelt: ein Dict mit einem Dict und einer Liste darin. Welche Kopierart erzeugt auch für die inneren Objekte neue Kopien? Schau auf Schritt 3.

import copy

def mit_port(konfig, port):
    neu = copy.deepcopy(konfig)
    neu["server"]["port"] = port
    return neu

mit_port

Übung 4: Dict und Set kombinieren

Schreibe abgleich(bestellt, geliefert). Beide Parameter sind Listen von Artikelnamen (Strings, mit möglichen Doppelten). Die Funktion liefert ein Dict mit drei Einträgen:

  • "fehlt": sortierte Liste der Artikel, die bestellt, aber nicht geliefert wurden (ohne Doppelte)
  • "zuviel": sortierte Liste der Artikel, die geliefert, aber nicht bestellt wurden (ohne Doppelte)
  • "doppelt": sortierte Liste der Artikel, die in geliefert mehr als einmal vorkommen (ohne Doppelte)

Für “fehlt” und “zuviel” sind Mengen praktisch, weil sie Doppelte entfernen und eine Differenz kennen. Für “doppelt” brauchst du eine Zählung, wie sie in Schritt 4 ein Dict macht. Achte auf die Richtung der Differenz und darauf, dass Mengen keine Reihenfolge haben.

def abgleich(bestellt, geliefert):
    b, g = set(bestellt), set(geliefert)
    zaehler = {}
    for x in geliefert:
        zaehler[x] = zaehler.get(x, 0) + 1
    return {
        "fehlt": sorted(b - g),
        "zuviel": sorted(g - b),
        "doppelt": sorted(k for k, n in zaehler.items() if n > 1),
    }

abgleich

Übung 5: Geld ohne Float-Fehler

Die Preise kommen als Floats mit zwei Nachkommastellen, zum Beispiel [0.29, 0.57]. Schreibe summe_in_cent(preise), die die Summe als ganze Zahl in Cent (int) liefert. Eine leere Liste ergibt 0.

Schau auf die Ausgabe von 0.29 * 100 in Schritt 5. Welche der beiden Umwandlungen (int oder round) schneidet ab, welche rundet? Und an welcher Stelle der Rechnung musst du sie anwenden: pro Preis oder erst am Ende?

def summe_in_cent(preise):
    return sum(round(p * 100) for p in preise)

summe_in_cent

Merksatz und Prüfstein

Merksatz: Ein Name ist nur ein Verweis, b = a kopiert nichts, eine flache Kopie teilt das Innere, und Float-Werte rechnest du nicht mit == und int(), sondern mit round() oder in ganzen Cent.

Prüfstein (offene Frage): Erkläre in eigenen Worten, warum ein Tupel als Dict-Schlüssel erlaubt ist, ([1], 2) aber nicht, obwohl beides ein Tupel ist. Was hat das mit “flach unveränderbar” und “hashbar” zu tun?

Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitte “Datentypen und Collections” mit “Begriffe rund um Referenzen”, “Liste”, “Dict”, “Set” und “Zahlen”. Hinweis: is für Identität, decimal, das Verhalten von t[0] += [5] im Tupel und die Aussage, dass round() rundet, stehen nicht in der Quelle, wurden aber mit Python 3.13 ausgeführt und geprüft (bitte prüfen, ob sie in der PCAP geprüft werden).