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:
ifnachforwird zum Filtern verwendet. Diese Klausel hat keinelse.a if cond else bvorforist ein bedingter Ausdruck. Diese Klausel braucht einelseund 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, verwendedefaultdictmit einerfor-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 duset()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 vonNoneund täuscht dem Leser vor, dass du ein Ergebnis brauchst. Nutze einefor-Schleife. - Wenn du mehr als zwei Ebenen verschachtelst oder mehrere verschachtelte
if-Anweisungen hast. Dreifor-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,Counteroder einefor-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 einefor-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 Namexnicht im äußeren Scope – anders als bei einer normalenfor-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.ifnachforfiltert,a if cond else bvorfortransformiert.- 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.