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.groupbymit Tupeln — Streaming-Gruppierung über sortierte Sequenzencollections.namedtuple— Felder gruppierter Datensätze benennencollections.Countermit 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:
- Zuerst sortieren. Ohne Sortierung erzeugt jeder neue Lauf desselben Schlüsselwerts eine separate Gruppe.
- Den Gruppen-Iterator sofort verbrauchen. Der innere Iterator
groupist 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=70000emp.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 timesCounter 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
| Ziel | Bestes Werkzeug |
|---|---|
| Gruppierung nach einem Feld aus einer Liste von Tupeln | defaultdict(list) |
| Gleichzeitige Gruppierung nach zwei oder mehr Feldern | defaultdict(list) mit Tupel-Schlüssel |
| Streaming-Gruppierung einer großen, vorsortierten Sequenz | itertools.groupby |
| Feldnamen zu gruppierten Datensätzen hinzufügen | collections.namedtuple |
| Vorkommen zusammengesetzter Ereignisse zählen | collections.Counter |
| Parallele Listen zu gruppierten Tupeln zusammenstellen | zip |
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
- Python Tuples — Tupel erstellen, indizieren und slicen
- Access Tuples — Indizierung, Slicing und Zugehörigkeitstest
- Loop Tuples — über ein Tupel mit
forundwhileiterieren - Unpack Tuples — Tupelelemente in einer Zeile Variablen zuweisen
- Update Tuples — Workarounds zum Ändern unveränderlicher Tupel
- Join Tuples — Konkatenation und der
tuple()-Konstruktor - Tuple Methods —
count()undindex()im Detail - Python Dictionaries — der Schlüssel-Wert-Speicher, der Tupel-Schlüssel-Gruppierung ermöglicht
- Python Lists Group — Listen mit
defaultdict,groupbyund Dict-Comprehensions gruppieren - Python Collections Module —
defaultdict,Counter,namedtupleund mehr