Zum Inhalt
wannacode

Lambdas

Kurz gesagt: Ein Lambda ist eine kurze Funktion ohne Namen mit einem einzigen Ausdruck, die du als Wert übergibst, zum Beispiel als key von sorted.

Problem

Eine ganze Funktion für eine Regel

Die Bestellungen sollen nach Summe sortiert werden.

Order
class Order:
def __init__(self, customer, total):
self.customer = customer
self.total = total
orders = [
Order("Maria", 75.50),
Order("Tom", 12.90),
Order("Laura", 40.00),
]
def by_total(order):
return order.total
orders.sort(key=by_total)
for o in orders:
print(f"{o.customer}: {o.total:.2f} €")
Ausgabe
Tom: 12.90 €
Laura: 40.00 €
Maria: 75.50 €

sort(key=...) nimmt eine Funktion, die für jedes Element sagt, wonach es sortiert wird. Hier ist das by_total: Für eine Bestellung gibt sie deren Summe zurück.

Es funktioniert. Aber für eine Regel („nach Summe“) schreibst du eine ganze Funktion mit Namen und Leerzeilen drumherum, weit weg von der Stelle, an der du sie benutzt. Und sie wird nur hier aufgerufen. Du willst nur die Regel übergeben.

Die Idee

Ein Lambda: nur die Regel

Dieselbe Sortierung in einer Zeile:

orders.sort(key=lambda o: o.total)

lambda o: o.total heißt Lambda-Ausdruck, kurz Lambda. Das ist eine Funktion ohne Namen: Nach lambda kommen die Parameter, nach dem Doppelpunkt der Rumpf. return schreibst du nicht: Der Wert des Rumpfs ist das Ergebnis.

Die Schreibweisen:

  • Ein Parameter: lambda o: o.total > 50. Mehrere: lambda a, b: a.total + b.total. Ohne Parameter: lambda: ....
  • Der Rumpf ist ein Ausdruck. Braucht die Regel mehrere Zeilen, ein if oder eine Schleife, ist sie eine normale Funktion mit def.
by_total = lambda o: o.total
total_of_two = lambda a, b: a.total + b.total
guest = lambda: Order("Gast", 0)
orders.sort(key=by_total)
for o in orders:
print(f"{o.customer}: {o.total:.2f} €")
print(total_of_two(orders[0], orders[1]))
print(guest().customer)
Ausgabe
Tom: 12.90 €
Laura: 40.00 €
Maria: 75.50 €
52.9
Gast

Ein Lambda ist ein Wert: Du kannst es in eine Variable legen und später übergeben. Das gilt auch für eine Funktion, die mit def geschrieben ist: by_total ohne Klammern ist die Funktion selbst, deshalb hat sort(key=by_total) funktioniert.

Genauer, für Neugierige

Der Stilleitfaden PEP 8 rät: Hat die Regel einen Namen, schreib sie mit def, nicht als name = lambda .... Ein Lambda ist für die Stelle, an der die Regel direkt übergeben wird, wie in sort(key=lambda o: o.total). In dieser Lektion stehen manche Regeln in Variablen, damit du siehst, dass ein Lambda ein Wert wie jeder andere ist.

Die Idee

Eine Funktion ist ein Wert

Woher weiß sort, dass lambda o: o.total die Schlüsselfunktion ist? Mehr muss es nicht wissen. In Python ist eine Funktion ein Wert, wie eine Zahl oder ein String: Du kannst sie in eine Variable legen, an eine andere Funktion übergeben und mit Klammern aufrufen.

student = lambda price: price * 0.8
minus_five = lambda price: price - 5
print(student(50))
print(minus_five(50))
Ausgabe
40.0
45

student ist ein Objekt vom Typ function. Du rufst es wie jede Funktion auf: student(50). Zwei Preisregeln und keine einzige Klasse.

Java braucht hier ein funktionales Interface: ein Interface mit einer Methode, das sagt, welche Art von Regel es ist. Python braucht so etwas nicht. Jede Funktion kann übergeben und aufgerufen werden.

student = lambda price: price * 0.8
minus_five = lambda price: price - 5
print(student(50))
print(minus_five(50))
Schritt für Schritt, in Worten
  1. Das Lambda ist ein Objekt vom Typ function, und student zeigt darauf. Sein Rumpf läuft noch nicht.
  2. Eine zweite Regel, ein zweites Objekt. Keine einzige Klasse ist geschrieben.
  3. student(50) übergibt 50 an das Lambda: price ist 50, und price * 0.8 ergibt 40.0.
  4. minus_five(50): dieselbe Zahl, eine andere Regel, 45.
Genauer, für Neugierige
  • Willst du aufschreiben, welche Regel du erwartest, hat Python einen Typhinweis (type hint): Callable[[float], float] heißt „eine Funktion, die eine Zahl nimmt und eine Zahl zurückgibt“. Python prüft ihn nicht. Das Falsche kommt bis zu der Stelle, an der es aufgerufen wird, und erst dort bricht das Programm ab:
from collections.abc import Callable
PriceRule = Callable[[float], float]
def show(rule: PriceRule, price: float):
print(rule(price))
show(lambda price: price * 0.8, 50)
show("Student", 50)
Ausgabe
40.0
Traceback (most recent call last):
...
TypeError: 'str' object is not callable
  • callable(x) sagt dir, ob man x aufrufen kann: callable(student) ist True, und callable("Student") ist False.

Die Idee

Die vier Arten von Regeln

Der Typ jeder Regel ist function, aber es gibt vier Arten von Regeln. Es hilft, sie zu erkennen, weil die Funktionen, die Regeln annehmen, eine bestimmte Art wollen:

  • Bedingung: nimmt einen Wert, gibt True oder False zurück. „Ist die Bestellung groß?“
  • Umwandlung: nimmt eine Sache, gibt eine andere zurück. Aus einer Bestellung ein Etikett.
  • Aktion: nimmt einen Wert und tut etwas damit: gibt ihn aus, speichert ihn. Ihr Ergebnis wird nicht benutzt.
  • Lieferant: nimmt nichts, gibt ein neues Objekt zurück, wenn du fragst.
big = lambda o: o.total >= 50
label = lambda o: o.customer.upper()
show = lambda text: print("-> " + text)
guest = lambda: Order("Gast", 0)
order = Order("Maria", 75.50)
print(big(order))
show(label(order))
print(guest().customer)
Ausgabe
True
-> MARIA
Gast

In Java sind Regeln Objekte mit Methoden, die sie zusammensetzen: Bedingungen verbindet man mit and, or und negate („nicht“), und Funktionen reiht man mit andThen hintereinander. Python hat solche Methoden nicht. Die neue Regel ist eine neue Funktion, die die alten aufruft: lambda o: big(o) and maria(o). Zwei Funktionen hintereinander setzt man auch mit einer Funktion zusammen:

price_with_vat = lambda price: price * 1.2
rounded = lambda price: round(price, 2)
then = lambda f, g: lambda x: g(f(x))
final_price = then(price_with_vat, rounded)
print(final_price(10.555))
Ausgabe
12.67

then nimmt zwei Funktionen und gibt eine dritte zurück: Sie ruft die erste auf und übergibt deren Ergebnis an die zweite.

Genauer, für Neugierige
  • Eine Aktion gibt trotzdem etwas zurück: None, weil das das Ergebnis von print ist. print(show("Kaffee")) gibt „-> Kaffee“ aus und dann None.
  • Das Modul operator hat fertige Funktionen für die häufigsten Umwandlungen. attrgetter("total") ist dasselbe wie lambda o: o.total.

Probier es aus

Zusammengesetzte Bedingungen

Was gibt das Programm aus?

big = lambda o: o.total >= 50
maria = lambda o: o.customer == "Maria"
big_and_maria = lambda o: big(o) and maria(o)
big_or_maria = lambda o: big(o) or maria(o)
not_big = lambda o: not big(o)
small = Order("Maria", 30)
large = Order("Tom", 80)
print(big_and_maria(small))
print(big_or_maria(small))
print(not_big(large))
Antwort

Sieh es dir an

Funktionen, die Funktionen annehmen

Viele fertige Funktionen nehmen statt einer Schleife eine Regel:
from collections import defaultdict
cart = ["Brot", "", "Milch", " ", "Kaffee"]
cart = list(filter(lambda item: item.strip() != "", cart))
for item in cart:
print(item)
print(max(cart, key=lambda item: len(item)))
by_city = defaultdict(lambda: [])
by_city["Berlin"].append("Maria")
by_city["Hamburg"].append("Tom")
by_city["Berlin"].append("Laura")
for city, names in sorted(by_city.items()):
print(f"{city}: {names}")
Ausgabe
Brot
Milch
Kaffee
Kaffee
Berlin: ['Maria', 'Laura']
Hamburg: ['Tom']
  • filter(bedingung, liste) lässt nur die Elemente durch, für die die Bedingung wahr ist. Es liefert sie eins nach dem anderen, deshalb sammelt list(...) sie in einer neuen Liste. Die alte ändert sich nicht: Hier zeigt cart einfach auf die neue.
  • max(liste, key=funktion) und min finden das größte und das kleinste Element nach der Regel. key ist dasselbe wie bei sort: Hier ist es die Länge, deshalb gewinnt „Kaffee“.
  • defaultdict(funktion) ist ein Dictionary, das die Funktion aufruft, wenn ein Schlüssel fehlt, das Ergebnis unter den Schlüssel legt und es zurückgibt. So funktioniert das Gruppieren nach Stadt ohne if city not in by_city. Hier ist die Funktion lambda: [], sie kann aber auch list selbst sein: Auch das gibt eine neue leere Liste zurück.
  • Python hat kein forEach. Für eine Aktion mit jedem Element nimmst du for.

Das Dictionary hält die Schlüssel in der Reihenfolge, in der sie hinzugefügt wurden. sorted(by_city.items()) sortiert sie, deshalb kommt Berlin vor Hamburg.

Die Idee

Eine Funktion über ihren Namen, ohne Klammern

Oft gibt ein Lambda seinen Parameter nur an eine vorhandene Funktion weiter: lambda s: int(s). Dann übergib die Funktion selbst, über ihren Namen und ohne Klammern:
names = ["maria", "tom"]
join = ", ".join
upper = str.upper
parse = int
new_list = list
print(join(names))
print(upper("laura"))
print(parse("42") + 1)
items = new_list()
items.append("Kaffee")
print(items)
Ausgabe
maria, tom
LAURA
43
['Kaffee']

Java hat dafür eine eigene Schreibweise, die Methodenreferenz mit ::. In Python reicht der Name. Die Arten sind dieselben vier:

  • eine Methode eines bestimmten Objekts: ", ".join ist lambda items: ", ".join(items);
  • eine Methode des Parameters: str.upper ist lambda s: s.upper();
  • eine Funktion: int ist lambda s: int(s);
  • eine Klasse: list ist lambda: list(). Eine Klasse ruft man wie eine Funktion auf, und sie gibt ein neues Objekt zurück.

Tut das Lambda etwas mehr, zum Beispiel lambda s: s.strip().upper(), lass es als Lambda stehen.

Sieh es dir an

Sortieren nach mehreren Feldern

key kann ein Tupel zurückgeben: mehrere Werte in Klammern. Tupel werden von links nach rechts verglichen: zuerst die ersten Werte, und sind sie gleich, die nächsten.

Die Schüler werden nach Note sortiert, von der höchsten an, und bei gleicher Note nach Name:

students = [
Student("Jonas", 5.50),
Student("Anna", 6.00),
Student("Peter", 5.50),
Student("Finn", 4.75),
]
students.sort(key=lambda s: (-s.grade, s.name))
Ausgabe
Anna 6.0
Jonas 5.5
Peter 5.5
Finn 4.75

-s.grade dreht nur die Richtung der Note um: Eine höhere Note ergibt eine kleinere Zahl und kommt nach vorn. Der Name bleibt alphabetisch. Kein einziges if.

Genauer, für Neugierige
  • Text kann man nicht mit einem Minus umdrehen. Aber das Sortieren in Python ist stabil: Gleiche Elemente bleiben in der Reihenfolge, in der sie waren. Deshalb geht „nach Note umgekehrt, dann nach Name“ auch mit zwei Sortierungen. Zuerst nach der zweiten Regel, dann nach der ersten:
students.sort(key=lambda s: s.name)
students.sort(key=lambda s: s.grade, reverse=True)
Ausgabe
Anna 6.0
Jonas 5.5
Peter 5.5
Finn 4.75
  • Eine Vergleichsfunktion wie compare in Java (eine negative Zahl, 0 oder eine positive Zahl) wird mit cmp_to_key aus dem Modul functools zu einem key:
from functools import cmp_to_key
def compare(a, b):
return (a.grade > b.grade) - (a.grade < b.grade)
students.sort(key=cmp_to_key(compare))
Ausgabe
Finn 4.75
Jonas 5.5
Peter 5.5
Anna 6.0

Probier es aus

Was kehrt reverse=True um?

Hier ist der Schlüssel (s.grade, s.name), ohne Minus, und reverse=True dreht die Richtung um.

Was gibt das Programm aus?

students = [
Student("Jonas", 5.50),
Student("Anna", 6.00),
Student("Peter", 5.50),
Student("Finn", 4.75),
]
students.sort(key=lambda s: (s.grade, s.name), reverse=True)
for s in students:
print(s.name, s.grade)
Antwort

Probier es aus

Finde den Fehler

Das Programm soll die Summe der Preise ausgeben. Es bricht schon beim ersten Preis mit einem Fehler ab. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Genauer, für Neugierige

Ein Lambda sieht die Variable selbst, keine Kopie. Ändert sich die Variable nach dem Lambda, sieht der nächste Aufruf den neuen Wert:

rate = 0.2
discount = lambda price: price * (1 - rate)
print(discount(100))
rate = 0.5
print(discount(100))
Ausgabe
80.0
50.0

In Java ist das genau wegen solcher Überraschungen verboten. Muss sich die Regel einen Wert merken, übergib ihn als Parameter.

Übung

Regeln als Werte

Löse die Aufgaben in deinem Editor.

1. Sortieren nach mehreren Feldern. Eine Klasse Product mit Name, Kategorie und Preis. Sortiere die Produkte nach Kategorie, innerhalb einer Kategorie vom teuersten an und bei gleichem Preis nach Name, mit einem sort und einem Tupel in key. Die Produkte sind: Maus, Zubehör, 25 €; Laptop, Computer, 1299 €; Tastatur, Zubehör, 49,90 €; Tablet, Computer, 399 €; Kopfhörer, Zubehör, 49,90 €.

Ausgabe
Computer | Laptop | 1299.00 €
Computer | Tablet | 399.00 €
Zubehör | Kopfhörer | 49.90 €
Zubehör | Tastatur | 49.90 €
Zubehör | Maus | 25.00 €

2. Bestellungen filtern. Gib Order eine Stadt. Schreib eine Funktion select(orders, rule), die eine neue Liste nur mit den Bestellungen zurückgibt, für die die Regel wahr ist. Mach zwei Regeln, hamburg und big (mindestens 50 €), und gib die Bestellungen aus, für die beide wahr sind, die außerhalb von Hamburg und die „in Hamburg oder mindestens 100 €“. Jede der drei Regeln ist ein Lambda, das hamburg und big aufruft. Die Bestellungen: Maria, Hamburg, 75,50 €; Tom, Dresden, 12,90 €; Laura, Hamburg, 40 €; Lukas, Berlin, 120 €.

Ausgabe
Hamburg, ab 50 €:
Maria, Hamburg: 75.50 €
Außerhalb von Hamburg:
Tom, Dresden: 12.90 €
Lukas, Berlin: 120.00 €
Kostenlose Lieferung (Hamburg oder ab 100 €):
Maria, Hamburg: 75.50 €
Laura, Hamburg: 40.00 €
Lukas, Berlin: 120.00 €
3. Rabatte nacheinander. Mach drei Rabatte als Lambdas: einen Studentenrabatt (−10 %), einen Black-Friday-Rabatt (−20 €) und „nicht unter null“. Schreib then(f, g), das eine Funktion zurückgibt: zuerst f, dann g. Verkette damit die Rabatte in zwei Reihenfolgen: zuerst den Studentenrabatt und zuerst den Freitagsrabatt. Probier es mit 120 € und mit 15 €. Spielt die Reihenfolge eine Rolle?
Ausgabe
120.00 € -> Student, dann Freitag: 88.00 €; Freitag, dann Student: 90.00 €
15.00 € -> Student, dann Freitag: 0.00 €; Freitag, dann Student: 0.00 €

Weitere Aufgaben

  • Eine Liste aufräumen. Entferne aus einer Liste von E-Mail-Adressen die leeren und die ohne @ mit einem einzigen filter und gib den Rest mit for aus.
  • Schüler nach Klasse. Gruppiere Schüler nach Klasse ("10A", "10B") in einem defaultdict(list) und gib die Klassen der Reihe nach mit sorted aus.
  • Validatoren. Mach ein Dictionary mit drei Validatoren als Lambdas: nicht leerer Text, nur Ziffern (str.isdigit), Länge bis 10. Probier jeden an ein paar Strings aus.

Test

Teste dich

1. Was darf im Rumpf eines lambda stehen?
2. Was davon ist eine Bedingung: nimmt eine Bestellung und gibt True oder False zurück?
3. Wodurch kann man lambda s: s.upper() ersetzen?
4. students.sort(key=lambda s: (s.grade, s.name), reverse=True). Was kehrt reverse=True um?
5. Warum bricht total += p in einer Funktion, die in einer Schleife aufgerufen wird, mit UnboundLocalError ab?

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