Python-Listen gruppieren
Drei Wege zum Gruppieren von Python-Listen: defaultdict, itertools.groupby und Dict-Comprehensions – mit Beispielen und häufigen Fehlern.
Das Gruppieren einer Liste bedeutet, ihre Elemente in Teilmengen aufzuteilen, die einen gemeinsamen Schlüssel teilen – zum Beispiel Wörter nach ihrem ersten Buchstaben oder Datensätze nach einem Kategoriefeld. Python bietet drei Hauptansätze: eine manuelle Schleife mit collections.defaultdict, itertools.groupby aus der Standardbibliothek und Dict-Comprehensions. Dieses Kapitel erklärt jede Technik, wann man welche wählt und welche Fallstricke zu vermeiden sind.
Verwandte Kapitel: Python-Listen · Listenmethoden · List Comprehension · Listen durchlaufen · collections-Modul
Was „Gruppieren" bedeutet
Gegeben eine flache Liste und eine Schlüsselfunktion, die jedes Element auf ein Gruppenlabel abbildet, besteht das Ziel darin, eine Zuordnung von jedem Label zur Liste der zugehörigen Elemente zu erzeugen:
['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
key = first letter
→ {'a': ['apple', 'avocado', 'apricot'],
'b': ['banana', 'blueberry'],
'c': ['cherry']}Die drei folgenden Techniken erzeugen alle diese Art von Ergebnis. Sie unterscheiden sich in Ausführlichkeit, Leistung und den Anforderungen an die Eingabe.
Technik 1: Manuelle Schleife mit defaultdict
collections.defaultdict ist der gebräuchlichste und flexibelste Ansatz. Wenn man auf einen Schlüssel zugreift, der noch nicht existiert, erstellt ein defaultdict(list) automatisch eine leere Liste für diesen Schlüssel – eine if key in d-Prüfung ist also nie nötig.
from collections import defaultdict
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
by_letter = defaultdict(list)
for word in words:
by_letter[word[0]].append(word)
for letter, group in sorted(by_letter.items()):
print(f'{letter}: {group}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']Warum defaultdict statt einem einfachen dict verwenden?
Mit einem einfachen dict ist vor dem ersten append eine explizite Prüfung nötig:
# Plain dict — more boilerplate, same result
by_letter = {}
for word in words:
if word[0] not in by_letter:
by_letter[word[0]] = []
by_letter[word[0]].append(word)Eine kürzere Alternative mit einem einfachen dict ist dict.setdefault:
by_letter = {}
for word in words:
by_letter.setdefault(word[0], []).append(word)setdefault ist für kurze Skripte in Ordnung, aber defaultdict ist schneller (keine wiederholten Schlüsselsuchen) und verdeutlicht die Absicht klarer.
Gruppieren nach einem berechneten Schlüssel
Der Schlüssel kann ein beliebiger Ausdruck sein, nicht nur ein Attribut. Hier wird eine Liste von Ganzzahlen in gerade und ungerade Gruppen aufgeteilt:
from collections import defaultdict
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
by_parity = defaultdict(list)
for n in numbers:
by_parity['even' if n % 2 == 0 else 'odd'].append(n)
print('even:', sorted(by_parity['even'])) # even: [2, 4, 6]
print('odd:', sorted(by_parity['odd'])) # odd: [1, 1, 3, 3, 5, 5, 5, 9]Eine Liste von Dicts gruppieren
Dies ist das häufigste Praxisszenario – Datenzeilen nach einem Feldwert gruppieren:
from collections import defaultdict
data = [
{'category': 'fruit', 'name': 'apple'},
{'category': 'vegetable', 'name': 'carrot'},
{'category': 'fruit', 'name': 'banana'},
{'category': 'vegetable', 'name': 'broccoli'},
]
grouped = defaultdict(list)
for item in data:
grouped[item['category']].append(item['name'])
for category, names in grouped.items():
print(f'{category}: {names}')
# fruit: ['apple', 'banana']
# vegetable: ['carrot', 'broccoli']Technik 2: itertools.groupby
itertools.groupby gruppiert aufeinanderfolgende Elemente, die denselben Schlüssel teilen. Es ist nützlich, wenn die Struktur von Folgeabschnitten erhalten bleiben soll oder wenn die Daten bereits sortiert sind und man vermeiden möchte, das gesamte Dictionary auf einmal aufzubauen (es ist lazy/streaming).
from itertools import groupby
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
# groupby only groups consecutive elements, so sort first
words_sorted = sorted(words, key=lambda w: w[0])
for letter, group in groupby(words_sorted, key=lambda w: w[0]):
print(f'{letter}: {list(group)}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']Der wichtige Fallstrick: vor groupby sortieren
groupby gruppiert nur aufeinanderfolgende Elemente mit demselben Schlüssel. Ist die Eingabe nicht nach dem Schlüssel sortiert, entstehen mehrere kleine Gruppen statt einer Gruppe pro Schlüssel:
from itertools import groupby
# Unsorted input — groupby produces WRONG results
numbers = [1, 1, 2, 3, 3, 1, 2, 2]
for key, group in groupby(numbers):
print(f'{key}: {list(group)}')
# 1: [1, 1] ← first run of 1s
# 2: [2]
# 3: [3, 3]
# 1: [1] ← second run of 1s — NOT merged with the first!
# 2: [2, 2]Immer nach derselben Schlüsselfunktion sortieren, bevor groupby aufgerufen wird:
numbers_sorted = sorted(numbers)
for key, group in groupby(numbers_sorted):
print(f'{key}: {list(group)}')
# 1: [1, 1, 1]
# 2: [2, 2, 2]
# 3: [3, 3]Wo groupby glänzt: Streaming großer Daten
Da groupby einen Iterator zurückgibt, werden nicht alle Gruppen gleichzeitig in den Speicher geladen. Das macht es nützlich für die zeilenweise Verarbeitung großer sortierter Dateien, ohne ein vollständiges Dictionary aufzubauen.
from itertools import groupby
# Grouping namedtuple records
from collections import namedtuple
Product = namedtuple('Product', ['category', 'name', 'price'])
products = [
Product('dairy', 'milk', 1.10),
Product('fruit', 'apple', 1.20),
Product('fruit', 'banana', 0.50),
Product('vegetable', 'broccoli', 1.50),
Product('vegetable', 'carrot', 0.80),
]
# products is already sorted by category here
for category, group in groupby(products, key=lambda p: p.category):
items = list(group)
print(f'{category}: {[p.name for p in items]}')
# dairy: ['milk']
# fruit: ['apple', 'banana']
# vegetable: ['broccoli', 'carrot']Technik 3: Dict-Comprehension
Eine Dict-Comprehension erstellt das gruppierte Dictionary in einem einzigen Ausdruck. Sie ist prägnant, hat aber einen Nachteil: Die innere List-Comprehension durchsucht für jeden eindeutigen Schlüssel erneut die gesamte Eingabe, was es zu O(n × k) macht, wobei k die Anzahl der eindeutigen Schlüssel ist. Für kleine Listen ist das in Ordnung; bei großen Listen sollte man defaultdict bevorzugen.
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
# Collect unique keys first, then build each group
letters = sorted(set(w[0] for w in words))
grouped = {letter: [w for w in words if w[0] == letter] for letter in letters}
for letter, group in grouped.items():
print(f'{letter}: {group}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']Diese Technik ist am lesbarsten, wenn der Schlüsselsatz klein und bereits bekannt ist – zum Beispiel beim Gruppieren von True/False-Ergebnissen oder einer festen Menge von Kategorien.
Gruppen nach dem Gruppieren aggregieren
Ein häufiger Folgeschritt nach dem Gruppieren ist die Aggregation: das Berechnen einer Summe, eines Durchschnitts, eines Minimums oder einer Anzahl pro Gruppe. defaultdict(list) lässt sich mit standardmäßiger Python-Arithmetik kombinieren:
from collections import defaultdict
scores = [
('Alice', 90), ('Bob', 75), ('Alice', 85),
('Bob', 88), ('Carol', 92),
]
by_student = defaultdict(list)
for name, score in scores:
by_student[name].append(score)
for student, student_scores in sorted(by_student.items()):
avg = sum(student_scores) / len(student_scores)
print(f'{student}: scores={student_scores}, avg={avg:.1f}')
# Alice: scores=[90, 85], avg=87.5
# Bob: scores=[75, 88], avg=81.5
# Carol: scores=[92], avg=92.0Die richtige Technik wählen
| Situation | Beste Wahl |
|---|---|
| Allgemeines Gruppieren, beliebige Reihenfolge | defaultdict(list) |
| Große sortierte Daten streamen | itertools.groupby |
| Kleine Liste, prägnanter Einzeiler | Dict-Comprehension |
| Eingabe ist bereits sortiert | defaultdict oder groupby |
| Aggregation nötig (Summe, Durchschnitt etc.) | defaultdict(list) + Arithmetik |
Häufige Fallstricke
Vergessen, vor groupby zu sortieren. groupby führt nur aufeinanderfolgende identische Schlüssel zusammen. Die Eingabe immer mit sorted() nach derselben Schlüsselfunktion sortieren, bevor sie an groupby übergeben wird.
list(group) nicht sofort zuweisen. Der Gruppeniterator von groupby ist erschöpft, sobald die äußere for-Schleife zum nächsten Schlüssel übergeht. Ihn innerhalb des Schleifenkörpers in eine Liste umwandeln, wenn er mehr als einmal verwendet werden soll.
Die Eingabeliste während des Gruppierens verändern. Das Hinzufügen oder Entfernen von Elementen aus der Liste während einer Gruppierungsschleife führt zu unvorhersehbaren Ergebnissen. Zuerst das gruppierte Dictionary aufbauen, dann Elemente verändern.
defaultdict erscheint in repr. defaultdict(list, {...}) sieht in repr anders aus als ein einfaches dict. Mit dict(grouped) umschließen, wenn eine einfache dict-Ausgabe benötigt wird.