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:
class Order: def __init__(self, customer, city, total): self.customer = customer self.city = city self.total = totalorders = [ 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)['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)['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 beifilter. Sie darf auch fehlen;o.customeram Anfang: was aus jedem Element wird, wie beimap;- 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:
filterundmap. 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)['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
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
- Fünf Preise. Die beiden Generatoren legen nur den Weg an: Noch ist nichts hindurchgegangen.
- 12 besteht if p >= 50 nicht und fällt heraus. Es wird nie zu Text.
- 75 besteht if, f"{p} €" macht daraus den Text „75 €“, list() sammelt ihn.
- Auch 40 fällt heraus. Jedes Element geht den ganzen Weg, bevor das nächste startet.
- 130 und 58 kommen durch. Die Liste prices ändert sich nicht.
- 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))Der Generator ist bereit, noch ist nichts hindurchgegangen.filter 12filter 75map 75filter 40[150]- Die Zeile „Der Generator ist bereit“ erscheint, bevor auch nur ein Element durch den Generator gelaufen ist.
map 75kommt direkt nachfilter 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)filter 12filter 75map 75filter 40Die Liste ist bereit.[150]- Deshalb kannst du aus einem Generator nur die ersten paar Werte nehmen, ohne die übrigen zu prüfen.
isliceaus dem Modulitertoolshö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)))filter 75filter 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}")Summe der geraden Zahlen: 6sum 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 0max_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} €")Bestellungen ab 50 €: 3Umsatz: 312.40 €Durchschnitt: 62.48 €Größte: 120.00 €lenliefert 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. maxeiner leeren Liste bricht mit einemValueErrorab.default=0sagt, 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)00ValueError: max() iterable argument is emptyZeroDivisionError: division by zeroDie 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}")[2, 3, 4, 5]['Lukas', 'Maria', 'Anna']Tisch 1Tisch 2Tisch 3set(...)entfernt die Wiederholungen: In einem Set (einer Menge) kommt jeder Wert nur einmal vor. Die Reihenfolge in einem Set ist nicht garantiert, deshalb kommt danachsorted.sorted(orders, key=..., reverse=True)sortiert ab der größten Bestellung, mitkeyaus der vorigen Lektion.[:3]nimmt die ersten drei Elemente. Das ist ein Slice (Ausschnitt): vom Anfang bis Index 3, ohne ihn. Zusammen mitsortedist 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)))[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)set entfernt die Wiederholungen: kaffee, tee, wasser, saft. sorted sortiert sie alphabetisch: kaffee, saft, tee, wasser. [:3] behält die ersten drei, und d.upper() schreibt sie groß.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")Größte: LukasFalsekeine gefundenmax(..., default=None)gibtNonezurück, statt abzubrechen, wenn die Liste leer ist.next(Generator, None)liefert den ersten Wert des Generators oderNone, wenn es keinen gibt. Das ist „die erste Bestellung über 500 €“, ohne die übrigen zu prüfen.is not Noneprüft, ob es einen Wert gibt. MitNonevergleicht man mitis, nicht mit==: So verlangt es PEP 8.x if Bedingung else yliefert 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 Nonemaria = nickname("maria")if maria is not None: print(f"Spitzname: {maria}")
tom = nickname("tom")print(tom if tom is not None else "kein Spitzname")Spitzname: Mimikein SpitznameGenauer, für Neugierige
- Wenn du die Prüfung vergisst:
huge = next((o for o in orders if o.total > 500), None)print(huge.customer)Traceback (most recent call last): ...AttributeError: 'NoneType' object has no attribute 'customer'x or yliefert ebenfalls einen Ersatzwert, behandelt aber auch einen leeren String und0als fehlend. Nimm es nur, wenn ein leerer String wirklich „fehlt“ bedeutet:
print(None or "kein Spitzname")print("" or "kein Spitzname")print(0 or 5)kein Spitznamekein Spitzname5any(...)undall(...)antworten mitTrueoderFalse, 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))TrueTrueProbier es aus
Finde den Fehler
Wähle die Zeile mit dem Fehler.
Ein Generator läuft nur einmal durch. list(expensive) in Zeile 3 verbraucht ihn, und in Zeile 4 ist nichts mehr in ihm übrig. Es gibt keine Fehlermeldung, deshalb ist so ein Versehen schwer zu finden:
Anzahl: 2Preise: []Brauchst du zwei Ergebnisse, mach in Zeile 2 mit eckigen Klammern eine Liste: [p for p in prices if p >= 50]. Die kannst du so oft benutzen, wie du willst: len(expensive) und expensive.
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.totalfor city, total in sorted(revenue_by_city.items()): print(f"{city}: {total:.2f} €")
customers = ", ".join(sorted(o.customer for o in orders))print(customers){'Berlin': 1, 'Dresden': 2, 'Hamburg': 2}Berlin: 120.00 €Dresden: 76.90 €Hamburg: 115.50 €Anna, Laura, Lukas, Maria, TomCounteraus dem Modulcollectionszählt: für jede Stadt, wie oft sie vorkommt. Er ist ein Dictionary.defaultdict(float)beginnt jede neue Stadt bei0.0(das gibtfloat()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()))){'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 wirfttoMapeine Exception):
last_by_city = {o.city: o.customer for o in orders}print(last_by_city){'Hamburg': 'Laura', 'Dresden': 'Anna', 'Berlin': 'Lukas'}Übung
Berichte mit List Comprehensions
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 €.
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).
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()].
4.50 5.75 6.00 3.00 5.50Durchschnitt: 4.95Höchste: 6.00Ausgezeichnet: 34. 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 _“.
Kaffee mit Milch, Kaffee ohne Zucker. Tee mit Honig!honig, kaffee, milch, mit, ohne, tee, zuckerWeitere 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 hilftdefault? - 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 mitall, ob alle mindestens 10 € betragen.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Listen: append, remove und sort · dict und set · Klassen und Daten in Klassen · Lambdas
- Geht weiter in: dataclass, match und Muster · Tests mit unittest · Datenbanken · Reflection und Dekoratoren
- Aufgaben zum Thema: Modernes Python