List, Dict, Set Comprehension in Python: kompakt und lesbar

1. Übersicht

Comprehension ist eine charakteristische Syntax in Python. Sie ermöglicht dir, Lists, Dicts oder Sets aus einem Iterable in einer einzigen Zeile zu erstellen. Wenn du sie richtig einsetzt, ist der Code kürzer und klarer als eine for-Schleife. Bei Missbrauch wird daraus aber eine lange Zeile, die schwer zu verstehen ist.

Dieser Artikel führt dich anhand konkreter Beispiele durch List, Dict und Set Comprehensions sowie Generator Expressions. Danach wirst du wissen, wann du Comprehensions einsetzen solltest, wann du zu for-Schleifen zurückkehrst, und wie du Tests für die Datentransformationslogik schreibst. Alle Beispiele laufen auf Python 3.12.

2. List Comprehension

2.1. Grundsyntax

Angenommen, du hast eine Liste von Bestellungen und möchtest den Gesamtbetrag jeder Bestellung abrufen. Die traditionelle for-Schleife sieht so aus:

from dataclasses import dataclass

@dataclass
class Order:
    id: int
    customer: str
    total: float
    status: str

orders = [
    Order(1, "An", 250_000, "PAID"),
    Order(2, "Bình", 90_000, "CANCELLED"),
    Order(3, "An", 1_200_000, "PAID"),
    Order(4, "Chi", 450_000, "PENDING"),
]

totals = []
for order in orders:
    totals.append(order.total)
print(totals)

Mit List Comprehension umgeschrieben:

totals = [order.total for order in orders]
print(totals)
[250000, 90000, 1200000, 450000]

Die Struktur ist [ausdruck for element in iterable]. Man liest es von links nach rechts: “nimm order.total für jede order in orders”. Du brauchst keine leere Liste zu deklarieren und musst auch append nicht aufrufen. Die Absicht “ich erstelle eine neue Liste” wird sofort durch die [ klar.

2.2. Elemente mit if filtern

Wenn du nur bezahlte Bestellungen abrufen möchtest, fügst du am Ende eine if-Klausel hinzu:

paid_ids = [order.id for order in orders if order.status == "PAID"]
print(paid_ids)
[1, 3]

Das if am Ende fungiert als Filter. Elemente, die die Bedingung nicht erfüllen, werden übersprungen, sodass die resultierende Liste kürzer als die ursprüngliche sein kann.

2.3. if-else-Ausdruck am Anfang

Wenn du alle Elemente entsprechend einer Bedingung transformieren möchtest, anstatt sie zu filtern, setzt du den if-else-Ausdruck vorne hin:

labels = ["groß" if order.total >= 500_000 else "klein" for order in orders]
print(labels)
['klein', 'klein', 'groß', 'klein']

Das ist eine häufige Verwechslung. Merke dir einfach zwei Regeln:

  • if nach for wird zum Filtern verwendet. Diese Klausel hat kein else.
  • a if cond else b vor for ist ein bedingter Ausdruck. Diese Klausel braucht ein else und behält die gleiche Anzahl von Elementen.

Wenn du [x for x in data if cond else y] schreibst, erhältst du einen SyntaxError.

3. Dict Comprehension

Dict Comprehension verwendet {} mit key: value-Paaren. Beispielsweise möchtest du Bestellungen schnell nach id nachschlagen:

orders_by_id = {order.id: order for order in orders}
print(orders_by_id[3])
Order(id=3, customer='An', total=1200000, status='PAID')

Du kannst auch ein vorhandenes Dict transformieren. Beispiel: 10% Steuern auf eine Preisliste anwenden:

prices = {"cafe": 30_000, "tra_sua": 45_000, "banh_mi": 20_000}
prices_with_vat = {name: round(price * 1.1) for name, price in prices.items()}
print(prices_with_vat)
{'cafe': 33000, 'tra_sua': 49500, 'banh_mi': 22000}

prices.items() gibt die (key, value)-Paare zurück, und du unpast sie direkt in der for-Schleife.

Ein nützlicher Trick ist das Umkehren eines Dicts:

status_codes = {"PAID": 1, "PENDING": 2, "CANCELLED": 3}
code_to_status = {code: status for status, code in status_codes.items()}
print(code_to_status[2])
PENDING

Hinweis: Wenn zwei Elemente denselben Schlüssel ergeben, wird das zweite Element das erste stillschweigend überschreiben. Beispiel: {order.customer: order.total for order in orders} behält nur die letzte Bestellung von “An” (1.200.000), und die 250.000-Bestellung verschwindet ohne Warnung. Wenn du Gruppen zusammenfassen musst, verwende defaultdict mit einer for-Schleife (siehe Abschnitt 7).

4. Set Comprehension

Set Comprehension verwendet auch {}, aber mit nur einem Ausdruck, ohne :. Das Ergebnis ist automatisch dedupliziert. Beispiel: Du möchtest wissen, welche Kunden bestellt haben:

customers = {order.customer for order in orders}
print(sorted(customers))
['An', 'Bình', 'Chi']

Jeder Kunde erscheint nur einmal, obwohl “An” zwei Bestellungen hat. Die Reihenfolge in einem Set ist nicht garantiert, daher verwendet das Beispiel sorted beim Ausgeben.

Set Comprehension ist sehr praktisch für schnelle Mitgliedschaftsprüfungen:

banned_emails = {"spam@x.com", "Fake@Y.com"}
normalized = {email.strip().lower() for email in banned_emails}
print("fake@y.com" in normalized)
True

Hinweis: Ein leeres {} ist ein Dict, kein Set. Um ein leeres Set zu erstellen, musst du set() schreiben.

5. Mehrere for-Schleifen und verschachtelte Comprehensions

Du kannst mehrere for in einer Comprehension schreiben. Die Lesereihenfolge ist genau die gleiche wie bei verschachtelten for-Schleifen von oben nach unten:

carts = {
    "An": ["cafe", "banh_mi"],
    "Chi": ["tra_sua"],
}

items = [(customer, item) for customer, products in carts.items() for item in products]
print(items)
[('An', 'cafe'), ('An', 'banh_mi'), ('Chi', 'tra_sua')]

Die obige Comprehension entspricht dem folgenden Code:

items = []
for customer, products in carts.items():
    for item in products:
        items.append((customer, item))

Der häufigste Fall ist das Flattening einer verschachtelten Liste:

matrix = [[1, 2, 3], [4, 5, 6]]
flat = [value for row in matrix for value in row]
print(flat)
[1, 2, 3, 4, 5, 6]

Merksatz: Die äußerste for-Schleife kommt zuerst. Wenn du zweimal hinschauen musst, um die Reihenfolge zu verstehen, ist das ein Zeichen dafür, dass du es in eine normale for-Schleife aufteilen solltest.

6. Generator Expression: wenn du nicht die ganze Liste brauchst

Wenn du [] durch () ersetzt, erhältst du einen Generator Expression. Python erstellt keine Liste im Speicher. Werte werden nacheinander erzeugt, wenn Code sie verbraucht.

revenue = sum(order.total for order in orders if order.status == "PAID")
print(revenue)
1450000

Wenn der Generator das einzige Argument einer Funktion ist, kannst du ein Paar Klammern weglassen. Dies funktioniert mit sum, any, all, max, min:

has_big_order = any(order.total > 1_000_000 for order in orders)
all_positive = all(order.total > 0 for order in orders)
print(has_big_order, all_positive)
True True

any stoppt sofort, wenn es das erste Element findet, das die Bedingung erfüllt. Mit großen Datenmengen spart ein Generator also nicht nur Speicher, sondern ist auch schneller als any([...]).

Hinweis: Ein Generator kann nur einmal durchlaufen werden. Ein zweiter Durchlauf gibt nichts zurück:

paid = (o for o in orders if o.status == "PAID")
print(len(list(paid)))
print(len(list(paid)))
2
0

Wenn du das Ergebnis mehrfach verwenden musst, nutze List Comprehension.

7. Comprehensions lesbar halten

7.1. Funktionen mit aussagekräftigen Namen für komplexe Logik

Lange Bedingungen machen Comprehensions schwer lesbar:

vip = [o.customer for o in orders if o.status == "PAID" and o.total >= 1_000_000 and not o.customer.startswith("test")]

Du kannst die Bedingung in eine Funktion mit aussagekräftigem Namen auslagern:

def is_vip_order(order: Order) -> bool:
    return (
        order.status == "PAID"
        and order.total >= 1_000_000
        and not order.customer.startswith("test")
    )

vip = [order.customer for order in orders if is_vip_order(order)]
print(vip)
['An']

Jetzt liest sich die Comprehension wie ein Satz, und die Geschäftslogik hat ihren eigenen Platz für Tests.

7.2. Sinnvolle Zeilenumbrüche

Comprehensions müssen nicht auf einer Zeile stehen. Innerhalb der Klammern kannst du beliebig umbrechen:

summary = {
    order.id: order.total
    for order in orders
    if order.status != "CANCELLED"
}
print(summary)
{1: 250000, 3: 1200000, 4: 450000}

Jede Zeile hat eine Aufgabe: Ausdruck, Quelle, Filter.

7.3. Walrus-Operator um doppelte Berechnung zu vermeiden

Manchmal brauchst du sowohl zu filtern als auch das Ergebnis derselben Berechnung zu verwenden. Der :=-Operator (Python 3.8+) erlaubt dir, es nur einmal zu berechnen:

def discount_for(order: Order) -> float:
    return order.total * 0.05 if order.total >= 400_000 else 0

discounts = {
    order.id: d
    for order in orders
    if (d := discount_for(order)) > 0
}
print(discounts)
{3: 60000.0, 4: 22500.0}

Nutze den Walrus-Operator sparsam. Die Variable d wird aus der Comprehension in den umgebenden Scope übergeben. Außerdem sind viele Leser mit dieser Syntax noch nicht vertraut.

8. Tests für Datentransformationslogik schreiben

Comprehensions findest du oft in Funktionen zur Datentransformation, daher solltest du sie wie jede andere Logik testen. Zuerst installierst du pytest:

pip install pytest

Angenommen, du hast das Modul reports.py:

# reports.py
from dataclasses import dataclass

@dataclass
class Order:
    id: int
    customer: str
    total: float
    status: str

def revenue_by_customer(orders: list[Order]) -> dict[str, float]:
    customers = {o.customer for o in orders if o.status == "PAID"}
    return {
        c: sum(o.total for o in orders if o.customer == c and o.status == "PAID")
        for c in customers
    }

Und eine entsprechende Testdatei:

# test_reports.py
from reports import Order, revenue_by_customer

def test_revenue_only_counts_paid_orders():
    orders = [
        Order(1, "An", 250_000, "PAID"),
        Order(2, "An", 1_200_000, "PAID"),
        Order(3, "Bình", 90_000, "CANCELLED"),
        Order(4, "Chi", 450_000, "PAID"),
    ]

    result = revenue_by_customer(orders)

    assert result == {"An": 1_450_000, "Chi": 450_000}
    assert "Bình" not in result

def test_empty_orders_returns_empty_dict():
    assert revenue_by_customer([]) == {}
pytest -q
..                                                                       [100%]
2 passed in 0.01s

Der erste Test behauptet zwei Dinge: stornierte Bestellungen werden nicht gezählt, und Kunden, die nur stornierte Bestellungen haben, erscheinen nicht im Ergebnis. Der zweite Test prüft den Grenzfall einer leeren Liste.

Die obige Funktion ist korrekt, aber hat eine Zeitkomplexität von O(n × Anzahl Kunden), weil jeder Kunde die ganze orders-Liste durchsucht. Mit großen Datenmengen ist eine for-Schleife mit defaultdict sowohl schneller als auch lesbarer:

from collections import defaultdict

def revenue_by_customer(orders: list[Order]) -> dict[str, float]:
    totals: dict[str, float] = defaultdict(float)
    for order in orders:
        if order.status == "PAID":
            totals[order.customer] += order.total
    return dict(totals)

Beide alten Tests bestehen noch, ohne dass du sie ändern musst. Das ist der Vorteil von Tests: du kannst selbstbewusst von Comprehension zu for-Schleife oder umgekehrt refaktorisieren.

9. Wann man Comprehensions NICHT verwenden sollte

  • Wenn du Seiteneffekte brauchst. Schreib nicht [print(o) for o in orders] oder [db.save(o) for o in orders]. Das erzeugt eine nutzlose Liste von None und täuscht dem Leser vor, dass du ein Ergebnis brauchst. Nutze eine for-Schleife.
  • Wenn du mehr als zwei Ebenen verschachtelst oder mehrere verschachtelte if-Anweisungen hast. Drei for-Schleifen in einer Zeile sind fast immer schwerer zu lesen als aufgeteilte Versionen.
  • Wenn du gruppieren oder akkumulieren musst, wie aufsummieren oder zählen. Nutze defaultdict, Counter oder eine for-Schleife.
  • Wenn du für jedes Element eine Exception behandeln musst. Comprehensions enthalten keine try/except-Blöcke. Lagere die Logik in eine separate Funktion aus und rufe sie in der Comprehension auf, oder nutze eine for-Schleife.
  • Wenn deine Daten riesig sind und du sie nur einmal durchsuchst. Nutze dann einen Generator Expression statt List Comprehension.

Hinweis: In Python 3 wird die Schleifenvariable einer Comprehension nicht nach außen bekanntgegeben. Nach [x for x in range(3)] existiert der Name x nicht im äußeren Scope – anders als bei einer normalen for-Schleife. Die einzige Ausnahme ist eine Variable, die mit := zugewiesen wird, wie in Abschnitt 7.3 erwähnt.

10. Fazit

Comprehensions helfen dir, die Absicht “eine neue Collection aus einer alten erstellen” kompakt auszudrücken:

  • [...] für Listen, {k: v ...} für Dicts, {...} für Sets, und (...) für Generatoren.
  • if nach for filtert, a if cond else b vor for transformiert.
  • Lagere komplexe Bedingungen in Funktionen mit aussagekräftigen Namen aus, und nutze Zeilenumbrüche bei langen Ausdrücken.
  • Nutze Generator Expressions mit sum, any, all, um Speicher zu sparen.
  • Geh zu for-Schleifen zurück, wenn du Seiteneffekte hast, wenn du gruppieren, Exceptions behandeln, oder wenn die Comprehension schwer lesbar wird.

Die einfachste Regel: Wenn ein Kollege deine Comprehension zweimal lesen muss, um sie zu verstehen, schreib sie als for-Schleife um. Code ist nur dann kurz wertvoll, wenn er immer noch lesbar ist.

pythoncomprehensionsauberer Codegeneratorpytest