Python-Kurs / Mit collections und itertools zählen, summieren und gruppieren

Mit collections zählen Sie Vorkommen oder sammeln Werte je Schlüssel; mit itertools verknüpfen und gruppieren Sie Iterationsabläufe. Wählen Sie diese Werkzeuge nach der Bedeutung des Ergebnisses und beachten Sie dabei Reihenfolge, Standardwerte und den Verbrauch von Iteratoren.

Vorkommen zählen oder Beträge aufsummieren

Eine Liste von Ausgaben kann zwei unterschiedliche Fragen beantworten: Wie viele Buchungen gehören zu einer Kategorie, und wie hoch ist deren Gesamtbetrag? Counter zählt die Kategorien. Ein defaultdict(int) eignet sich dagegen zum schrittweisen Addieren der Beträge. Zwei Buchungen sind eine Anzahl; 11.500 ist im folgenden Beispiel die Summe ihrer ganzzahligen Beträge.

Ein defaultdict besitzt eine Standardfabrik: eine aufrufbare Funktion, die bei einem fehlenden Schlüssel über den Zugriff mit eckigen Klammern einen Anfangswert erzeugt. Hier liefert int() die Zahl 0. Deshalb kann totals[category] += amount auch für eine neue Kategorie unmittelbar addieren. Übergeben wird int, also die Funktion selbst, nicht das Ergebnis int().

Das bloße Lesen mit totals["other"] kann somit einen neuen Eintrag erzeugen. totals.get("other") ruft diese Fabrik nicht auf und liefert ohne eigenen Ersatzwert None, wenn der Schlüssel fehlt. Falls eine Leseoperation Ihre Daten nicht verändern soll, berücksichtigen Sie diesen Unterschied oder verwenden Sie ein gewöhnliches Dictionary mit einer ausdrücklich formulierten Akkumulation.

Zählung, Summe und Verkettung vollständig ausführen

Speichern Sie die folgende vollständige Datei als collections_demo.py in einem eigenen Übungsordner mit UTF-8. Die verwendeten Module gehören zur Standardbibliothek; zusätzliche Pakete sind nicht erforderlich. Starten Sie in Ihrer Projektumgebung python collections_demo.py. Außerhalb einer virtuellen Umgebung kann unter macOS oder Ubuntu der Befehl python3 verwendet werden müssen.

from collections import Counter, defaultdict
from itertools import chain, groupby

rows = [("food", 3500), ("bus", 2500), ("food", 8000)]
counts = Counter(category for category, _ in rows)
totals = defaultdict(int)
for category, amount in rows:
    totals[category] += amount
print(sorted(counts.items()))
print(sorted(totals.items()))
print(list(chain([1, 2], [3])))
for category, group in groupby(sorted(rows), key=lambda row: row[0]):
    print(category, sum(amount for _, amount in group))

Erwartete Ausgabe:

[('bus', 1), ('food', 2)]
[('bus', 2500), ('food', 11500)]
[1, 2, 3]
bus 2500
food 11500

Die ersten beiden Zeilen ordnen die Schlüssel für eine übersichtliche Ausgabe mit sorted. Die Ausgangsliste wird dadurch nicht verändert. chain([1, 2], [3]) liest zunächst die erste, dann die zweite Quelle; list(...) sammelt die ausgegebenen Elemente für die Anzeige. Die letzten beiden Zeilen summieren Kategorien über benachbarte Gruppen.

Das Unterstrich-Ziel _ ist hier nur ein gewöhnlicher Variablenname für einen im jeweiligen Ausdruck nicht benötigten Wert. Die Beträge bleiben in diesem Beispiel ganze Zahlen in einer einheitlichen Einheit. Die Werkzeuge selbst prüfen keine fachlichen Betragsgrenzen oder Währungen.

groupby benötigt aufeinanderfolgende gleiche Schlüssel

itertools.groupby fasst zusammenhängende Abschnitte mit demselben Schlüssel zusammen. Es sammelt verstreute gleiche Kategorien nicht automatisch wie eine SQL-Gruppierung über alle Datensätze ein. In der ursprünglichen Reihenfolge food, bus, food entstehen daher drei Gruppen. Sortieren Sie für eine gemeinsame Kategoriesumme zuerst nach derselben Kategorie, die auch groupby verwendet.

Im ersten Programm sortiert sorted(rows) die Tupel lexikografisch. Dadurch liegen die Kategorien hier bereits zusammen. Der folgende Vergleich drückt die Absicht durch sorted(rows, key=category) deutlicher aus. category(row) gibt das erste Tupelfeld zurück; lambda row: row[0] ist eine kurze namenlose Funktion mit demselben Ergebnis. Speichern Sie das vollständige zweite Programm als groups.py und starten Sie python groups.py.

from itertools import groupby
rows = [("food", 3500), ("bus", 2500), ("food", 8000)]
def category(row):
    return row[0]
for label, data in [("original", rows), ("sorted", sorted(rows, key=category))]:
    print(label)
    for name, group in groupby(data, key=category):
        print(name, sum(amount for _, amount in group))
print(category(rows[0]) == (lambda row: row[0])(rows[0]))

Die Ausgabe zeigt den Unterschied unmittelbar:

original
food 3500
bus 2500
food 8000
sorted
bus 2500
food 11500
True

Die Bezeichnungen original und sorted stammen aus dem Programm. True bestätigt den Vergleich der beiden Schlüsselfunktionen für die erste Zeile. Das Beispiel zeigt keine automatische Umsortierung durch groupby: Die Sortierung erfolgt ausdrücklich vorher.

Jede Gruppe ist außerdem ein Iterator, der den zugrunde liegenden Datenfluss mit der äußeren Gruppierung teilt. Wenn Sie zur nächsten Gruppe weitergehen, können noch nicht gelesene Elemente der vorherigen Gruppe verlorengehen. Bilden Sie daher die Summe sofort, wie im Beispiel, oder speichern Sie die jeweilige Gruppe rechtzeitig als Liste. Eine Liste aller Paare aus Schlüssel und noch ungelesenem Gruppeniterator bewahrt deren Inhalte nicht verlässlich auf.

Die Referenz zu itertools.groupby beschreibt die gemeinsamen Datenquellen; die Referenz zu defaultdict erläutert die Standardfabrik. Den einmaligen Verbrauch behandelt auch der Kurs zu Iteratoren und Generatoren.

Speicherbedarf und Kontrollversuche berücksichtigen

Ein Iterator muss nicht seine vollständige Ausgabe im Voraus als Liste speichern. Das macht eine gesamte Verarbeitung jedoch nicht automatisch speichersparend: sorted(rows) erzeugt hier eine neue sortierte Liste, und ein abschließendes list(...) hält alle erzeugten Ergebnisse. Ein Dictionary für laufende Kategoriesummen benötigt dagegen Einträge für die vorkommenden Kategorien. Wählen Sie nach der tatsächlich benötigten Ausgabe und der Datenmenge.

itertools.islice kann die Zahl der entnommenen Elemente begrenzen. Das ist besonders bei unendlichen Quellen wie itertools.count() nötig: Wandeln Sie eine solche Quelle niemals ohne Grenze vollständig in eine Liste um. Auch chain kommt zur zweiten Quelle erst, wenn die erste beendet ist; eine unendliche erste Quelle verhindert dies.

  1. Entfernen Sie im ersten Programm nur sorted aus der Eingabe für groupby. Die Zähl- und Dictionary-Summen bleiben unverändert, aber am Ende erscheinen zwei getrennte food-Gruppen mit 3500 und 8000. Stellen Sie danach die Sortierung wieder her.
  2. Lesen Sie in einer eigenen Python-Sitzung einen fehlenden Schlüssel eines leeren defaultdict(int) zunächst mit get und anschließend mit eckigen Klammern. Vergleichen Sie jeweils den Rückgabewert und die vorhandenen Schlüssel.
  3. Ändern Sie nur den ersten Betrag von 3500 auf 4000. Die Anzahl der food-Buchungen muss zwei bleiben, ihre Summe aber auf 12.000 steigen. Setzen Sie den Betrag anschließend zurück und vergleichen Sie die ursprüngliche Ausgabe.