W3docs

Daten mit Python-Tupeln gruppieren

Daten mit Python-Tupeln gruppieren: Tupel als Dict-Schlüssel, Mehrfeldgruppierung, itertools.groupby, namedtuple, Counter und zip.

Tupel eignen sich hervorragend zur Gruppierung von Daten in Python. Da ein Tupel unveränderlich und hashbar ist, kann es als Dictionary-Schlüssel dienen — etwas, das eine Liste niemals kann. Tupel sind daher die natürliche Wahl, wenn Datensätze nach einer Kombination von Feldern gruppiert, mehrdimensionale Koordinaten verfolgt oder zusammengesetzte Ereignisse gezählt werden sollen.

Dieses Kapitel behandelt vier praktische Gruppierungsmuster:

  • Tupel als Dictionary-Schlüssel — Einfeldgruppierung und Mehrfeldgruppierung
  • itertools.groupby mit Tupeln — Streaming-Gruppierung über sortierte Sequenzen
  • collections.namedtuple — Felder gruppierter Datensätze benennen
  • collections.Counter mit Tupeln — zusammengesetzte Ereignisse zählen

Verwandte Kapitel: Python Tuples · Access Tuples · Loop Tuples · Python Dictionaries · Python Lists Group

Warum Tupel als Dictionary-Schlüssel dienen können

Python verlangt, dass Dictionary-Schlüssel hashbar sind — ihr Wert darf sich nach dem Speichern des Schlüssels nicht mehr ändern. Tupel erfüllen diese Bedingung, weil sie unveränderlich sind. Listen erfüllen sie nicht und lösen einen TypeError aus, wenn man versucht, sie als Schlüssel zu verwenden.

# A tuple can be a dictionary key
coordinates = {}
coordinates[(10, 20)] = "warehouse A"
coordinates[(30, 40)] = "warehouse B"
print(coordinates[(10, 20)])  # warehouse A

# A list cannot be a dictionary key
try:
    d = {[10, 20]: "warehouse A"}
except TypeError as e:
    print(f"TypeError: {e}")
# TypeError: unhashable type: 'list'

Ein wichtiger Fallstrick: Ein Tupel, das ein veränderbares Element (z. B. eine Liste) enthält, ist ebenfalls nicht hashbar und kann nicht als Schlüssel verwendet werden:

try:
    d = {(1, [2, 3]): "value"}
except TypeError as e:
    print(f"TypeError: {e}")
# TypeError: unhashable type: 'list'

Tupel-Schlüssel sollten ausschließlich aus unveränderlichen Werten bestehen — Strings, Zahlen, boolean-Werte oder andere Tupel.

Gruppierung nach einem einzelnen Tupelfeld

Der einfachste Anwendungsfall ist das Entpacken einer Sequenz von Tupeln und die Gruppierung nach einem Element. Mit collections.defaultdict(list) vermeidet man den Boilerplate-Code, der prüft, ob ein Schlüssel bereits vorhanden ist.

from collections import defaultdict

employees = [
    ("Alice", "Engineering"),
    ("Bob", "Marketing"),
    ("Carol", "Engineering"),
    ("Dave", "Marketing"),
    ("Eve", "Engineering"),
]

by_dept = defaultdict(list)
for name, dept in employees:
    by_dept[dept].append(name)

for dept, members in sorted(by_dept.items()):
    print(f"{dept}: {members}")
# Engineering: ['Alice', 'Carol', 'Eve']
# Marketing: ['Bob', 'Dave']

defaultdict(list) erstellt beim ersten Auftreten eines neuen dept-Schlüssels automatisch eine leere Liste, sodass keine if dept not in by_dept-Prüfung notwendig ist.

Mehrfeldgruppierung mit einem Tupel-Schlüssel

Die eigentliche Stärke von Tupel-Schlüsseln zeigt sich, wenn nach mehr als einem Feld gleichzeitig gruppiert werden soll. Die Felder werden zu einem Tupel zusammengefasst und dieses Tupel als Dictionary-Schlüssel verwendet.

from collections import defaultdict

records = [
    ("Alice", "Engineering", "Senior"),
    ("Bob", "Marketing", "Junior"),
    ("Carol", "Engineering", "Junior"),
    ("Dave", "Marketing", "Senior"),
    ("Eve", "Engineering", "Senior"),
]

# Group by (department, level) — a two-field composite key
grouped = defaultdict(list)
for name, dept, level in records:
    grouped[(dept, level)].append(name)

for (dept, level), names in sorted(grouped.items()):
    print(f"{dept} / {level}: {names}")
# Engineering / Junior: ['Carol']
# Engineering / Senior: ['Alice', 'Eve']
# Marketing / Junior: ['Bob']
# Marketing / Senior: ['Dave']

Da (dept, level) selbst ein Tupel ist, ist es hashbar und kann als Dict-Schlüssel dienen, unabhängig davon, wie viele Felder es enthält. Das Destrukturieren des Schlüssels mit for (dept, level), names in ... hält den Code lesbar.

Raster- und Koordinatengruppierung

Mehrfeldgruppierung mit Tupeln eignet sich auch natürlich für räumliche Daten:

points = [(0, 0), (1, 2), (0, 1), (1, 3), (2, 4)]

from collections import defaultdict

by_x = defaultdict(list)
for x, y in points:
    by_x[x].append(y)

for x, ys in sorted(by_x.items()):
    print(f"x={x}: y-values={ys}")
# x=0: y-values=[0, 1]
# x=1: y-values=[2, 3]
# x=2: y-values=[4]

Gruppierung mit itertools.groupby

itertools.groupby gruppiert aufeinanderfolgende Elemente, die denselben Schlüssel haben. Die Funktion ist speichereffizient, weil sie lazy arbeitet — sie lädt nicht alle Gruppen auf einmal in den Speicher. Der Nachteil ist, dass die Eingabe nach demselben Schlüssel sortiert sein muss, bevor sie an groupby übergeben wird; andernfalls entstehen mehrere Teilgruppen für denselben Schlüssel statt einer einzigen.

from itertools import groupby

sales = [
    ("East", "Q1", 1200),
    ("East", "Q2", 1500),
    ("West", "Q1", 900),
    ("West", "Q2", 1100),
    ("East", "Q3", 1800),
]

# Sort by region (index 0) before grouping
sales_sorted = sorted(sales, key=lambda t: t[0])

for region, group in groupby(sales_sorted, key=lambda t: t[0]):
    items = list(group)
    total = sum(q[2] for q in items)
    print(f"{region}: total={total}, quarters={[q[1] for q in items]}")
# East: total=4500, quarters=['Q1', 'Q2', 'Q3']
# West: total=2000, quarters=['Q1', 'Q2']

Zwei Dinge sind bei der Verwendung von groupby mit Tupeln zu beachten:

  1. Zuerst sortieren. Ohne Sortierung erzeugt jeder neue Lauf desselben Schlüsselwerts eine separate Gruppe.
  2. Den Gruppen-Iterator sofort verbrauchen. Der innere Iterator group ist erschöpft, wenn die äußere Schleife zum nächsten Schlüssel wechselt. list(group) sollte immer innerhalb des Schleifenkörpers aufgerufen werden, bevor er anderswo verwendet wird.

Wann groupby gegenüber defaultdict vorzuziehen ist

groupby empfiehlt sich bei der Verarbeitung einer großen, bereits sortierten Sequenz, bei der das vollständige gruppierte Ergebnis nicht in den Speicher geladen werden soll. Für die allgemeine Gruppierung ohne Sortiergarantie ist defaultdict(list) einfacher und zuverlässiger.

Gruppierung mit collections.namedtuple

namedtuple ermöglicht es, Tupelfeldern Namen zu geben, wodurch gruppierte Daten selbstdokumentierend werden. Sobald der namedtuple-Typ definiert ist, verhalten sich Instanzen genau wie normale Tupel — sie sind unveränderlich, hashbar und iterierbar —, aber auf Felder kann zusätzlich per Name statt nur per Index zugegriffen werden.

from collections import namedtuple, defaultdict

Employee = namedtuple("Employee", ["name", "department", "salary"])

employees = [
    Employee("Alice", "Engineering", 95000),
    Employee("Bob", "Marketing", 72000),
    Employee("Carol", "Engineering", 88000),
    Employee("Dave", "Marketing", 68000),
    Employee("Eve", "Engineering", 102000),
]

by_dept = defaultdict(list)
for emp in employees:
    by_dept[emp.department].append(emp)

for dept, members in sorted(by_dept.items()):
    avg_salary = sum(e.salary for e in members) / len(members)
    print(f"{dept}: {[e.name for e in members]}, avg salary={avg_salary:.0f}")
# Engineering: ['Alice', 'Carol', 'Eve'], avg salary=95000
# Marketing: ['Bob', 'Dave'], avg salary=70000

emp.department und emp.salary sind deutlich lesbarer als emp[1] und emp[2]. Der namedtuple-Ansatz ist besonders nützlich, wenn das Tupel viele Felder hat und die Positionsindizierung unübersichtlich wird.

Zusammengesetzte Ereignisse mit Counter zählen

collections.Counter zählt hashbare Objekte. Wenn das zu zählende „Objekt" eine Kombination von Werten ist, werden diese Werte in ein Tupel gepackt und die Tupelsequenz an Counter übergeben.

from collections import Counter

log = [
    ("GET", 200),
    ("POST", 201),
    ("GET", 200),
    ("GET", 404),
    ("POST", 500),
    ("GET", 200),
    ("DELETE", 204),
]

counts = Counter(log)
for entry, n in counts.most_common():
    method, status = entry
    print(f"{method} {status}: {n} times")
# GET 200: 3 times
# POST 201: 1 times
# GET 404: 1 times
# POST 500: 1 times
# DELETE 204: 1 times

Counter verwendet das Tupel intern als Hash-Schlüssel, sodass jede eindeutige (method, status)-Kombination separat verfolgt wird — ohne manuellen Gruppierungscode.

Tupelgruppen mit zip zusammenstellen

zip verbindet Elemente aus zwei oder mehr Sequenzen zu Tupeln. Dies ist eine natürliche Methode, um gruppierte Datensätze aus parallelen Listen zusammenzustellen, bevor eine Gruppierungsoperation angewendet wird.

from collections import defaultdict

names = ["Alice", "Bob", "Carol"]
scores = [95, 87, 92]
departments = ["Engineering", "Marketing", "Engineering"]

# Pair the three sequences into tuples
records = list(zip(names, scores, departments))
print(records)
# [('Alice', 95, 'Engineering'), ('Bob', 87, 'Marketing'), ('Carol', 92, 'Engineering')]

# Now group by department
by_dept = defaultdict(list)
for name, score, dept in records:
    by_dept[dept].append((name, score))

for dept, members in sorted(by_dept.items()):
    print(f"{dept}: {members}")
# Engineering: [('Alice', 95), ('Carol', 92)]
# Marketing: [('Bob', 87)]

Das richtige Gruppierungswerkzeug wählen

ZielBestes Werkzeug
Gruppierung nach einem Feld aus einer Liste von Tupelndefaultdict(list)
Gleichzeitige Gruppierung nach zwei oder mehr Felderndefaultdict(list) mit Tupel-Schlüssel
Streaming-Gruppierung einer großen, vorsortierten Sequenzitertools.groupby
Feldnamen zu gruppierten Datensätzen hinzufügencollections.namedtuple
Vorkommen zusammengesetzter Ereignisse zählencollections.Counter
Parallele Listen zu gruppierten Tupeln zusammenstellenzip

Häufige Fallstricke

Vergessen, vor groupby zu sortieren. itertools.groupby fasst nur aufeinanderfolgende identische Schlüssel zusammen. Erscheint derselbe Schlüssel an mehreren nicht aufeinanderfolgenden Positionen, wird jeder Lauf zu einer separaten Gruppe. Vor dem Aufruf von groupby immer nach derselben Schlüsselfunktion sortieren.

Ein veränderbares Element in einem Tupel-Schlüssel verwenden. Ein Tupel, das eine Liste enthält, ist nicht hashbar und löst einen TypeError aus, wenn es als Dict-Schlüssel verwendet wird. Tupel-Schlüssel sollten aus Strings, Zahlen, boolean-Werten oder verschachtelten Tupeln bestehen.

Den groupby-Iterator mehr als einmal verbrauchen. Der Gruppen-Subiterator von groupby ist erschöpft, sobald die äußere Schleife weiterrückt. list(group) im Schleifenkörper aufrufen, wenn die Gruppe mehr als einmal durchlaufen werden soll.

Die Ausgabe von defaultdict wie ein normales dict behandeln. Ein defaultdict erstellt beim Zugriff auf einen fehlenden Schlüssel automatisch neue Einträge, was den Dict stillschweigend mit leeren Listen befüllen kann. Um das Vorhandensein eines Schlüssels zu prüfen, ohne neue Einträge zu erzeugen, zuerst in ein normales dict umwandeln: dict(grouped).

Verwandte Themen

Übungen

Übung
Which of the following can be used as a Python dictionary key?
Which of the following can be used as a Python dictionary key?
Was this page helpful?