Zum Inhalt
wannacode

List Comprehensions und Generatoren

Kurz gesagt: Eine List Comprehension macht aus einer alten Liste in einer Zeile eine neue, mit Bedingung und Umwandlung, und ein Generator liefert die Werte einzeln nacheinander.

Problem

Ein Bericht in einer Schleife

Die Bestellungen haben jetzt auch eine Stadt:

Order
class Order:
def __init__(self, customer, city, total):
self.customer = customer
self.city = city
self.total = total
orders = [
Order("Maria", "Hamburg", 75.50),
Order("Tom", "Dresden", 12.90),
Order("Laura", "Hamburg", 40.00),
Order("Lukas", "Berlin", 120.00),
Order("Anna", "Dresden", 64.00),
]

Du willst die Kunden mit Bestellungen ab 50 €, alphabetisch sortiert:

result = []
for o in orders:
if o.total >= 50:
result.append(o.customer)
result.sort()
print(result)
Ausgabe
['Anna', 'Lukas', 'Maria']

Sechs Zeilen, und die Frage ist nur eine. Der Code sagt, wie: eine leere Liste anlegen, durchlaufen, prüfen, hinzufügen, sortieren. Um zu verstehen, was er tut, musst du ihn im Kopf ausführen. Kommt „nur die ersten drei“ oder „ohne Wiederholungen“ dazu, wächst die Schleife weiter.

Die Idee

Eine List Comprehension: was, nicht wie

Derselbe Bericht, in einer Zeile:

result = sorted([o.customer for o in orders if o.total >= 50])
print(result)
Ausgabe
['Anna', 'Lukas', 'Maria']

[o.customer for o in orders if o.total >= 50] heißt List Comprehension. Sie liest sich fast wie ein Satz: „der Kunde jeder Bestellung o in orders, wenn ihr Betrag mindestens 50 ist“. Die Teile sind:

  • for o in orders: woher die Elemente kommen;
  • if o.total >= 50: welche bleiben. Das ist eine Bedingung, wie bei filter. Sie darf auch fehlen;
  • o.customer am Anfang: was aus jedem Element wird, wie bei map;
  • die eckigen Klammern: Das Ergebnis ist eine neue Liste.

sorted(...) sortiert und gibt ebenfalls eine neue Liste zurück. Die Liste orders ändert sich nicht.

Genauer, für Neugierige
  • Dieselben Schritte gibt es auch als Funktionen mit Lambdas aus der vorigen Lektion: filter und map. Das Ergebnis ist dasselbe, aber man liest es von innen nach außen, deshalb schreibt man in Python meist eine List Comprehension:
result = sorted(map(lambda o: o.customer, filter(lambda o: o.total >= 50, orders)))
print(result)
Ausgabe
['Anna', 'Lukas', 'Maria']
  • Ist der Ausdruck der einzige Parameter einer Funktion, kannst du die eckigen Klammern weglassen: sorted(o.customer for o in orders if o.total >= 50) ergibt dasselbe. Dann ist der Ausdruck ein Generator. Um ihn geht es in der nächsten Karte.

Sieh es dir an

Was durch den Filter kommt

Hier sind die Schritte zwei Generatoren: Ausdrücke in runden statt in eckigen Klammern. Ein Generator macht keine Liste. Er liefert die Werte einzeln nacheinander, wenn jemand nach ihnen fragt, und berechnet sie erst dann.

prices = [12, 75, 40, 130, 58]
expensive = (p for p in prices if p >= 50)
labels = (f"{p} €" for p in expensive)
print(list(labels))
Schritt für Schritt, in Worten
  1. Fünf Preise. Die beiden Generatoren legen nur den Weg an: Noch ist nichts hindurchgegangen.
  2. 12 besteht if p >= 50 nicht und fällt heraus. Es wird nie zu Text.
  3. 75 besteht if, f"{p} €" macht daraus den Text „75 €“, list() sammelt ihn.
  4. Auch 40 fällt heraus. Jedes Element geht den ganzen Weg, bevor das nächste startet.
  5. 130 und 58 kommen durch. Die Liste prices ändert sich nicht.
  6. Das Ergebnis ist eine neue Liste mit drei Strings.

Die fünf Preise laufen einer nach dem anderen. 12 kommt bei der Prüfung if p >= 50 an und fällt heraus. 75 kommt durch, der zweite Generator macht daraus den Text „75 €“, und list() sammelt ihn. Dann fällt 40 heraus, und 130 und 58 kommen durch.

Jedes Element geht den ganzen Weg, bevor das nächste startet. Und nichts startet, bis list() nach den Werten fragt.

Genauer, für Neugierige

Das heißt Lazy Evaluation (bedarfsgesteuerte Auswertung). Der Generator merkt sich nur, was zu tun ist. Sieh es dir mit einer Ausgabe in jedem Schritt an:

def check(p):
print("filter", p)
return p >= 50
def double(p):
print("map", p)
return p * 2
stream = (double(p) for p in [12, 75, 40] if check(p))
print("Der Generator ist bereit, noch ist nichts hindurchgegangen.")
print(list(stream))
Ausgabe
Der Generator ist bereit, noch ist nichts hindurchgegangen.
filter 12
filter 75
map 75
filter 40
[150]
  • Die Zeile „Der Generator ist bereit“ erscheint, bevor auch nur ein Element durch den Generator gelaufen ist.
  • map 75 kommt direkt nach filter 75: Die Elemente warten nicht, bis alle die Prüfung passiert haben.
  • Mit eckigen Klammern ist derselbe Ausdruck eine Liste und wird sofort berechnet, in der Zeile, in der er steht:
def check(p):
print("filter", p)
return p >= 50
def double(p):
print("map", p)
return p * 2
now = [double(p) for p in [12, 75, 40] if check(p)]
print("Die Liste ist bereit.")
print(now)
Ausgabe
filter 12
filter 75
map 75
filter 40
Die Liste ist bereit.
[150]
  • Deshalb kannst du aus einem Generator nur die ersten paar Werte nehmen, ohne die übrigen zu prüfen. islice aus dem Modul itertools hört auf, sobald es so viele genommen hat, wie es braucht:
from itertools import islice
def check(p):
print("filter", p)
return p >= 50
stream = (p for p in [75, 130, 58, 12, 99] if check(p))
print(list(islice(stream, 2)))
Ausgabe
filter 75
filter 130
[75, 130]

Die Idee

Summe, Anzahl, Durchschnitt

Das Ergebnis kann statt einer Liste auch eine Zahl sein. Die Summe der geraden Zahlen:

numbers = [1, 2, 3, 4, 5]
total = sum(n for n in numbers if n % 2 == 0)
print(f"Summe der geraden Zahlen: {total}")
Ausgabe
Summe der geraden Zahlen: 6
sum addiert die Zahlen, die es bekommt. Der Generator darin hat keine zusätzlichen Klammern, weil er ihr einziger Parameter ist.

Dasselbe für die Bestellungen:

big = len([o for o in orders if o.total >= 50])
revenue = sum(o.total for o in orders)
average = revenue / len(orders) if orders else 0
max_total = max((o.total for o in orders), default=0)
print(f"Bestellungen ab 50 €: {big}")
print(f"Umsatz: {revenue:.2f} €")
print(f"Durchschnitt: {average:.2f} €")
print(f"Größte: {max_total:.2f} €")
Ausgabe
Bestellungen ab 50 €: 3
Umsatz: 312.40 €
Durchschnitt: 62.48 €
Größte: 120.00 €
  • len liefert die Anzahl. Hier zählen wir die Elemente einer List Comprehension.
  • Der Durchschnitt ist die Summe geteilt durch die Anzahl. Gibt es keine Bestellungen, bricht die Division durch 0 das Programm ab, daher if orders else 0.
  • max einer leeren Liste bricht mit einem ValueError ab. default=0 sagt, was gelten soll, wenn es keinen Wert gibt. Das ist die nächste Idee.
Genauer, für Neugierige

Mit einer leeren Liste:

prices = []
print(sum(prices))
print(max(prices, default=0))
try:
print(max(prices))
except ValueError as e:
print("ValueError:", e)
try:
print(sum(prices) / len(prices))
except ZeroDivisionError as e:
print("ZeroDivisionError:", e)
Ausgabe
0
0
ValueError: max() iterable argument is empty
ZeroDivisionError: division by zero

Die Idee

Sortieren, abschneiden, ohne Wiederholungen

ratings = [5, 3, 5, 4, 2, 4, 5]
print(sorted(set(ratings)))
top = [o.customer for o in sorted(orders, key=lambda o: o.total, reverse=True)[:3]]
print(top)
for i in range(1, 4):
print(f"Tisch {i}")
Ausgabe
[2, 3, 4, 5]
['Lukas', 'Maria', 'Anna']
Tisch 1
Tisch 2
Tisch 3
  • set(...) entfernt die Wiederholungen: In einem Set (einer Menge) kommt jeder Wert nur einmal vor. Die Reihenfolge in einem Set ist nicht garantiert, deshalb kommt danach sorted.
  • sorted(orders, key=..., reverse=True) sortiert ab der größten Bestellung, mit key aus der vorigen Lektion.
  • [:3] nimmt die ersten drei Elemente. Das ist ein Slice (Ausschnitt): vom Anfang bis Index 3, ohne ihn. Zusammen mit sorted ist das eine „Top 3“.
  • range(1, 4) liefert die Zahlen 1, 2, 3. Das Ende ist nicht dabei.
Genauer, für Neugierige

Willst du die Wiederholungen entfernen, aber die Reihenfolge des ersten Auftretens behalten, macht dict.fromkeys ein Dictionary mit den Schlüsseln in der Reihenfolge des Hinzufügens:

ratings = [5, 3, 5, 4, 2, 4, 5]
print(list(dict.fromkeys(ratings)))
Ausgabe
[5, 3, 4, 2]

Probier es aus

Was kommt am Ende an?

Was gibt das Programm aus?

drinks = ["kaffee", "tee", "kaffee", "wasser", "tee", "saft"]
result = [d.upper() for d in sorted(set(drinks))[:3]]
print(result)
Antwort

Die Idee

None: ein Ergebnis, das fehlen kann

Welche Bestellung ist die größte? Ist die Liste leer, gibt es keine Antwort. Dann ist das Ergebnis None: „nichts“. Aber der Aufrufer muss daran denken, es zu prüfen. Vergisst er es, bricht das Programm irgendwann später mit einem AttributeError ab.

Java hat dafür Optional: eine Box, die entweder einen Wert enthält oder leer ist. Python hat keine solche Box. Ein fehlender Wert ist None, und die Prüfung ist deine Aufgabe, an der Stelle, an der das Ergebnis benutzt wird:

biggest = max(orders, key=lambda o: o.total, default=None)
if biggest is not None:
print(f"Größte: {biggest.customer}")
huge = next((o for o in orders if o.total > 500), None)
print(huge is not None)
print(huge.customer if huge is not None else "keine gefunden")
Ausgabe
Größte: Lukas
False
keine gefunden
  • max(..., default=None) gibt None zurück, statt abzubrechen, wenn die Liste leer ist.
  • next(Generator, None) liefert den ersten Wert des Generators oder None, wenn es keinen gibt. Das ist „die erste Bestellung über 500 €“, ohne die übrigen zu prüfen.
  • is not None prüft, ob es einen Wert gibt. Mit None vergleicht man mit is, nicht mit ==: So verlangt es PEP 8.
  • x if Bedingung else y liefert den Ersatzwert, wenn es keinen Wert gibt.

Auch deine eigenen Funktionen geben None zurück, wenn sie keine Antwort haben:

def nickname(user):
# Gibt den Spitznamen zurück oder None, wenn die Person keinen hat.
return "Mimi" if user == "maria" else None
maria = nickname("maria")
if maria is not None:
print(f"Spitzname: {maria}")
tom = nickname("tom")
print(tom if tom is not None else "kein Spitzname")
Ausgabe
Spitzname: Mimi
kein Spitzname
Genauer, für Neugierige
  • Wenn du die Prüfung vergisst:
huge = next((o for o in orders if o.total > 500), None)
print(huge.customer)
Fehler
Traceback (most recent call last):
...
AttributeError: 'NoneType' object has no attribute 'customer'
  • x or y liefert ebenfalls einen Ersatzwert, behandelt aber auch einen leeren String und 0 als fehlend. Nimm es nur, wenn ein leerer String wirklich „fehlt“ bedeutet:
print(None or "kein Spitzname")
print("" or "kein Spitzname")
print(0 or 5)
Ausgabe
kein Spitzname
kein Spitzname
5
  • any(...) und all(...) antworten mit True oder False, ob irgendein oder jedes Element die Bedingung erfüllt:
print(any(o.total > 100 for o in orders))
print(all(o.total >= 10 for o in orders))
Ausgabe
True
True

Probier es aus

Finde den Fehler

Das Programm soll ausgeben, wie viele Preise teuer sind, und die Preise selbst. Die Anzahl stimmt, aber die Preise sind eine leere Liste. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Gruppieren und verbinden

Wie viele Bestellungen gibt es aus jeder Stadt, und wie hoch ist der Umsatz?

from collections import Counter, defaultdict
count_by_city = Counter(o.city for o in orders)
print(dict(sorted(count_by_city.items())))
revenue_by_city = defaultdict(float)
for o in orders:
revenue_by_city[o.city] += o.total
for city, total in sorted(revenue_by_city.items()):
print(f"{city}: {total:.2f} €")
customers = ", ".join(sorted(o.customer for o in orders))
print(customers)
Ausgabe
{'Berlin': 1, 'Dresden': 2, 'Hamburg': 2}
Berlin: 120.00 €
Dresden: 76.90 €
Hamburg: 115.50 €
Anna, Laura, Lukas, Maria, Tom
  • Counter aus dem Modul collections zählt: für jede Stadt, wie oft sie vorkommt. Er ist ein Dictionary.
  • defaultdict(float) beginnt jede neue Stadt bei 0.0 (das gibt float() zurück), und += addiert den Betrag.
  • ", ".join(...) verbindet die Strings zu einem, mit einem Komma dazwischen.

Das Dictionary behält die Schlüssel in der Reihenfolge des Hinzufügens. sorted(dictionary.items()) liefert die Paare aus Schlüssel und Wert nach dem Schlüssel sortiert, deshalb kommt der Bericht alphabetisch heraus.

Genauer, für Neugierige
  • Für jede Stadt eine Liste der Namen:
from collections import defaultdict
names_by_city = defaultdict(list)
for o in orders:
names_by_city[o.city].append(o.customer)
print(dict(sorted(names_by_city.items())))
Ausgabe
{'Berlin': ['Lukas'], 'Dresden': ['Tom', 'Anna'], 'Hamburg': ['Maria', 'Laura']}
  • Eine Dictionary Comprehension macht aus einer Liste ein Dictionary: {schlüssel: wert for ...}. Wiederholt sich ein Schlüssel, gewinnt der letzte Wert, ohne Fehler (in Java wirft toMap eine Exception):
last_by_city = {o.city: o.customer for o in orders}
print(last_by_city)
Ausgabe
{'Hamburg': 'Laura', 'Dresden': 'Anna', 'Berlin': 'Lukas'}

Übung

Berichte mit List Comprehensions

Löse die Aufgaben in deinem Editor.

1. Verkäufe nach Kategorie. Eine Klasse Sale mit Produkt, Kategorie, Menge und Stückpreis und einer Methode revenue() (Menge mal Preis). Addiere den Umsatz pro Kategorie mit defaultdict(float), gib die Kategorien alphabetisch und den Gesamtumsatz aus. Die Verkäufe: Kaffeebohnen (Kategorie „Kaffee und Tee“), 12 Stück zu 18,50 €; Tee („Kaffee und Tee“), 30 zu 4,20 €; Schokolade („Snacks“), 40 zu 2,80 €; Kekse („Snacks“), 25 zu 3,10 €; Honig („Feinkost“), 8 zu 12 €; noch einmal Kaffeebohnen, 5 zu 18,50 €.

Ausgabe
Feinkost: 96.00 €
Kaffee und Tee: 440.50 €
Snacks: 189.50 €
Gesamt: 726.00 €

2. Die Top 3 der Produkte. Finde mit denselben Verkäufen die drei Produkte mit dem höchsten Umsatz. Der Kaffee steht in zwei Verkäufen, deshalb gruppierst du zuerst nach Produkt und addierst, dann sortierst du die Einträge der Map nach dem Wert. Tipp: sorted(by_product.items(), key=lambda item: item[1], reverse=True)[:3]: Jedes Element von items() ist ein Paar (Produkt, Umsatz).

Ausgabe
Kaffeebohnen: 314.50 €
Tee: 126.00 €
Schokolade: 112.00 €

3. Die Durchschnittsnote. Lies eine Zeile mit Noten, getrennt durch Leerzeichen. Gib den Durchschnitt aus, die höchste Note und wie viele ausgezeichnet sind (5,50 und mehr). Tipp: [float(g) for g in input().split()].

Eingabe
4.50 5.75 6.00 3.00 5.50
Ausgabe
Durchschnitt: 4.95
Höchste: 6.00
Ausgezeichnet: 3

4. Die Wörter in einem Text. Lies eine Zeile Text und gib die Wörter darin ohne Wiederholungen aus, alphabetisch, kleingeschrieben, durch Kommas getrennt. Finde die Folgen von Buchstaben mit re.findall(r"[^\W\d_]+", text): \w ist ein Buchstabe, eine Ziffer oder _, und [^\W\d_] ist „ein Buchstabe, aber keine Ziffer und kein _“.

Eingabe
Kaffee mit Milch, Kaffee ohne Zucker. Tee mit Honig!
Ausgabe
honig, kaffee, milch, mit, ohne, tee, zucker

Weitere Aufgaben

  • Das längste Wort. Finde mit max(words, key=len) das längste Wort in einem Satz. Was passiert bei einer leeren Zeile, und wie hilft default?
  • Durchschnitt pro Klasse. Schüler mit Name, Klasse und Note: Sammle die Noten jeder Klasse in einem defaultdict(list) und gib den Durchschnitt jeder Klasse aus.
  • Große und kleine Bestellungen. Prüfe mit any, ob es eine Bestellung über 1000 € gibt, und mit all, ob alle mindestens 10 € betragen.

Test

Teste dich

1. Was passiert mit der Liste orders nach [o for o in orders if o.total > 50]?
2. Was ist der Unterschied zwischen [p * 2 for p in prices] und (p * 2 for p in prices)?
3. Was macht max(orders, key=lambda o: o.total) mit einer leeren Liste?
4. Warum benutzt der Bericht nach Städten sorted(revenue_by_city.items())?
5. expensive = (p for p in prices if p >= 50); print(len(list(expensive))); print(list(expensive)). Was gibt die zweite Zeile aus?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·