23. First-Class-Funktionen und funktionale Techniken
In den vorherigen Kapiteln haben wir gelernt, wie man Funktionen definiert und aufruft, mit Parametern und Argumenten arbeitet und den Geltungsbereich von Variablen (variable scope) versteht. Jetzt werden wir eine leistungsstarke Eigenschaft erkunden, die Python auszeichnet: Funktionen sind First-Class-Objekte (first-class objects). Das bedeutet, Funktionen können wie jeder andere Wert behandelt werden—sie können in Variablen gespeichert, als Argumente an andere Funktionen übergeben und von Funktionen zurückgegeben werden.
Diese Fähigkeit eröffnet elegante Programmiertechniken, die Code flexibler, wiederverwendbarer und ausdrucksstärker machen. Wir werden untersuchen, wie Sie First-Class-Funktionen (first-class functions) anhand praktischer Beispiele nutzen, Closures (closures), also Funktionen, die sich ihre Umgebung „merken“, verstehen, Lambda-Ausdrücke (lambda expressions) für prägnante Funktionsdefinitionen verwenden und eingebaute Funktionen wie map() und filter() anwenden, um effizient mit Sammlungen zu arbeiten.
23.1) Funktionen als First-Class-Objekte
23.1.1) Was „First-Class“ bedeutet
In Python sind Funktionen First-Class-Objekte, was bedeutet, dass sie:
- Variablen zugewiesen werden können
- In Datenstrukturen (Listen, Dictionaries usw.) gespeichert werden können
- Als Argumente an andere Funktionen übergeben werden können
- Als Werte von anderen Funktionen zurückgegeben werden können
Das unterscheidet sich von einigen Programmiersprachen, in denen Funktionen einen Sonderstatus haben und nicht wie normale Werte manipuliert werden können. In Python ist eine Funktion einfach ein weiterer Objekttyp, ähnlich wie Integer, Strings oder Listen.
Sehen wir uns das in Aktion an:
# Eine einfache Funktion definieren
def greet(name):
return f"Hello, {name}!"
# Die Funktion einer Variable zuweisen
say_hello = greet
# Die Funktion über die neue Variable aufrufen
message = say_hello("Alice")
print(message) # Output: Hello, Alice!
# Prüfen, dass beide Namen auf dieselbe Funktion verweisen
print(greet) # Output: <function greet at 0x...>
print(say_hello) # Output: <function greet at 0x...>
print(greet is say_hello) # Output: TrueBeachten Sie, dass wir bei say_hello = greet die Funktion nicht aufrufen (keine Klammern). Wir erstellen einen neuen Namen, der auf dasselbe Funktionsobjekt verweist. Sowohl greet als auch say_hello zeigen jetzt auf dieselbe Funktion, was wir mit dem Operator is überprüfen können.
23.1.2) Funktionen in Datenstrukturen speichern
Da Funktionen Objekte sind, können wir sie in Listen, Dictionaries oder jeder anderen Sammlung (Collection) speichern:
# Rechner mit Operationen, die in einem Dictionary gespeichert sind
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
def divide(x, y):
return x / y
# Funktionen in einem Dictionary speichern
operations = {
'+': add,
'-': subtract,
'*': multiply,
'/': divide
}
# Das Dictionary verwenden, um Berechnungen durchzuführen
num1 = 10
num2 = 5
operator = '*'
result = operations[operator](num1, num2)
print(f"{num1} {operator} {num2} = {result}") # Output: 10 * 5 = 50Dieses Muster ist äußerst nützlich, um flexible Systeme zu bauen. Statt lange Ketten von if-elif-Anweisungen zu schreiben, um auszuwählen, welche Funktion aufgerufen werden soll, können wir die passende Funktion in einem Dictionary nachschlagen und direkt aufrufen.
23.2) Funktionen als Argumente übergeben
23.2.1) Das Grundkonzept
Eine der mächtigsten Anwendungen von First-Class Functions ist, sie als Argumente an andere Funktionen zu übergeben. Dadurch können wir flexiblen, wiederverwendbaren Code schreiben, der mit unterschiedlichen Verhaltensweisen arbeiten kann.
Hier ist ein einfaches Beispiel:
# Funktion, die eine andere Funktion auf einen Wert anwendet
def apply_operation(value, operation):
"""Wendet die als Parameter übergebene Funktion auf den Wert an."""
return operation(value)
# Verschiedene Operationen
def double(x):
return x * 2
def square(x):
return x * x
def negate(x):
return -x
# Dieselbe apply_operation-Funktion mit verschiedenen Operationen verwenden
number = 5
print(apply_operation(number, double)) # Output: 10
print(apply_operation(number, square)) # Output: 25
print(apply_operation(number, negate)) # Output: -5Die Funktion apply_operation weiß nicht und es ist ihr egal, welche konkrete Operation sie ausführt. Sie ruft einfach die Funktion auf, die ihr übergeben wird. Diese Trennung der Verantwortlichkeiten macht Code modularer und leichter erweiterbar.
23.2.2) Sammlungen mit benutzerdefinierten Funktionen verarbeiten
Ein häufiges Muster ist, jedes Element in einer Sammlung mit einer als Argument übergebenen Funktion zu verarbeiten:
# Jedes Element in einer Liste mit einer gegebenen Funktion verarbeiten
def process_list(items, processor):
"""Wendet die processor-Funktion auf jedes Element in der Liste an."""
results = []
for item in items:
results.append(processor(item))
return results
# Verschiedene Verarbeitungsfunktionen
def uppercase(text):
return text.upper()
def add_exclamation(text):
return text + "!"
def get_length(text):
return len(text)
# Dieselbe Liste auf verschiedene Arten verarbeiten
words = ["hello", "world", "python"]
print(process_list(words, uppercase)) # Output: ['HELLO', 'WORLD', 'PYTHON']
print(process_list(words, add_exclamation)) # Output: ['hello!', 'world!', 'python!']
print(process_list(words, get_length)) # Output: [5, 5, 6]Dieses Muster ist so nützlich, dass Python eingebaute Funktionen wie map() und filter() bereitstellt, die auf diese Weise arbeiten (wir werden diese in Abschnitt 23.6 erkunden).
23.2.3) Sortieren durch Bereitstellen einer Key-Funktion (Kurze Einführung)
Pythons Funktion sorted() akzeptiert einen Parameter key—eine Funktion, die bestimmt, wie Elemente verglichen werden:
# Schüler nach verschiedenen Kriterien sortieren
students = [
{"name": "Alice", "grade": 85, "age": 20},
{"name": "Bob", "grade": 92, "age": 19},
{"name": "Charlie", "grade": 78, "age": 21},
{"name": "Diana", "grade": 95, "age": 20}
]
# Funktion, um die Note zu extrahieren
def get_grade(student):
return student["grade"]
# Funktion, um den Namen zu extrahieren
def get_name(student):
return student["name"]
# Nach Note sortieren (aufsteigend)
by_grade = sorted(students, key=get_grade)
print("Sorted by grade:")
for student in by_grade:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95
# Nach Name sortieren (alphabetisch)
by_name = sorted(students, key=get_name)
print("\nSorted by name:")
for student in by_name:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Bob: 92
# Charlie: 78
# Diana: 95Die key-Funktion wird für jedes Element genau einmal aufgerufen, und ihr Rückgabewert wird für den Vergleich verwendet. Das ist viel flexibler, als eigene Sortierlogik schreiben zu müssen.
Dieses Muster, Funktionen zu übergeben, um Verhalten anzupassen, ist in Python äußerst verbreitet. Wir werden fortgeschrittenere Sortiertechniken in Kapitel 38 erkunden.
23.3) Funktionen aus Funktionen zurückgeben
23.3.1) Funktionen, die Funktionen erzeugen
So wie wir Funktionen als Argumente übergeben können, können wir auch Funktionen aus anderen Funktionen zurückgeben. Dadurch können wir dynamisch spezialisierte Funktionen erzeugen:
# Funktion, die eine neue Funktion erzeugt und zurückgibt
def create_multiplier(factor):
"""Erstellt eine Funktion, die mit dem gegebenen factor multipliziert."""
def multiplier(x):
return x * factor
return multiplier
# Spezialisierte Multiplikator-Funktionen erstellen
double = create_multiplier(2)
triple = create_multiplier(3)
times_ten = create_multiplier(10)
# Die erzeugten Funktionen verwenden
print(double(5)) # Output: 10
print(triple(5)) # Output: 15
print(times_ten(5)) # Output: 50Was passiert hier? Die Funktion create_multiplier definiert eine innere Funktion namens multiplier und gibt sie zurück. Jedes Mal, wenn wir create_multiplier mit einem anderen Faktor aufrufen, bekommen wir eine neue Funktion zurück, die sich diesen spezifischen Faktor „merkt“. Das ist unser erster Einblick in Closures, die wir im nächsten Abschnitt ausführlich betrachten werden.
23.3.2) Benutzerdefinierte Validatoren erstellen
Das Zurückgeben von Funktionen ist besonders nützlich, um benutzerdefinierte Validierungs- oder Verarbeitungsfunktionen zu erzeugen:
# Bereichsvalidatoren dynamisch erstellen
def create_range_validator(min_value, max_value):
"""Erstellt eine Funktion, die prüft, ob eine Zahl im Bereich liegt."""
def validator(number):
return min_value <= number <= max_value
return validator
# Spezifische Validatoren erstellen
is_valid_age = create_range_validator(0, 120)
is_valid_percentage = create_range_validator(0, 100)
is_room_temperature = create_range_validator(15, 30)
# Die Validatoren verwenden
age = 25
print(f"Is {age} a valid age? {is_valid_age(age)}") # Output: True
temp = 22
print(f"Is {temp}°C room temperature? {is_room_temperature(temp)}") # Output: True
score = 150
print(f"Is {score} a valid percentage? {is_valid_percentage(score)}") # Output: False23.4) Closures verstehen: Funktionen, die sich erinnern
23.4.1) Was ist eine Closure?
Eine Closure ist eine Funktion, die Variablen aus dem Scope, in dem sie erstellt wurde, „merkt“, selbst nachdem dieser Scope fertig ausgeführt wurde. In den Beispielen aus Abschnitt 23.3 haben wir Closures bereits verwendet, ohne sie explizit so zu nennen.
Sehen wir uns an, wie Closures funktionieren:
def create_counter(start=0):
"""Erstellt eine Zählerfunktion, die sich ihren count merkt."""
count = start # Diese Variable wird von der Closure „eingefangen“
def counter():
nonlocal count # Auf die eingefangene Variable zugreifen
count += 1
return count
return counter
# Zwei unabhängige Zähler erstellen
counter1 = create_counter(0)
counter2 = create_counter(100)
# Jeder Zähler verwaltet seinen eigenen count
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter1()) # Output: 3
print(counter2()) # Output: 101
print(counter2()) # Output: 102
print(counter1()) # Output: 4 (counter1 is independent of counter2)Die innere Funktion counter bildet eine Closure über die Variable count. Obwohl create_counter die Ausführung beendet hat, hat die zurückgegebene Funktion counter weiterhin Zugriff auf count. Jeder Aufruf von create_counter erzeugt eine neue, unabhängige Closure mit ihrer eigenen count-Variable.
23.4.2) Wie Closures Variablen einfangen
Wenn eine Funktion innerhalb einer anderen Funktion definiert wird, kann sie auf Variablen aus dem Scope der äußeren Funktion zugreifen. Diese Variablen werden „eingefangen“ und bleiben auch dann zugänglich, nachdem die äußere Funktion zurückkehrt:
Wenn Python die innere Funktion erstellt, speichert es nicht nur den Funktionscode—es speichert auch Referenzen auf alle Variablen aus der äußeren Funktion, die die innere Funktion verwendet. Dieser Prozess wird als „capturing“ von Variablen bezeichnet.
def create_greeter(greeting):
"""Erstellt eine Begrüßungsfunktion mit einer benutzerdefinierten greeting."""
def greet(name):
return f"{greeting}, {name}!"
return greet
# Verschiedene Greeter erstellen
say_hello = create_greeter("Hello")
say_hi = create_greeter("Hi")
say_bonjour = create_greeter("Bonjour")
# Jeder Greeter merkt sich seine spezifische Begrüßung
print(say_hello("Alice")) # Output: Hello, Alice!
print(say_hi("Bob")) # Output: Hi, Bob!
print(say_bonjour("Claire")) # Output: Bonjour, Claire!Der Parameter greeting wird von der Closure eingefangen. Jede Greeter-Funktion hat ihren eigenen eingefangenen greeting-Wert, den sie bei jedem Aufruf verwendet.
23.4.3) Praktischer Einsatz: Konfigurationsfunktionen
Closures eignen sich hervorragend, um Funktionen mit vorkonfiguriertem Verhalten zu erstellen:
# Preisrechner mit unterschiedlichen Steuersätzen erstellen
def create_price_calculator(tax_rate):
"""Erstellt einen Rechner, der einen bestimmten Steuersatz anwendet."""
def calculate_total(price):
tax = price * tax_rate
return price + tax
return calculate_total
# Rechner für verschiedene Regionen erstellen
us_calculator = create_price_calculator(0.07) # 7% tax
uk_calculator = create_price_calculator(0.20) # 20% VAT
japan_calculator = create_price_calculator(0.10) # 10% consumption tax
# Preise in verschiedenen Regionen berechnen
item_price = 100
print(f"US total: ${us_calculator(item_price):.2f}") # Output: US total: $107.00
print(f"UK total: £{uk_calculator(item_price):.2f}") # Output: UK total: £120.00
print(f"Japan total: ¥{japan_calculator(item_price):.2f}") # Output: Japan total: ¥110.0023.4.4) Wann man Closures verwenden sollte
Closures sind besonders nützlich, wenn Sie:
- Funktionen mit vorkonfiguriertem Verhalten erstellen müssen
- Zustand zwischen Funktionsaufrufen halten möchten, ohne Klassen zu verwenden
- Callback-Funktionen implementieren, die sich Kontext merken müssen
- Function-Factories erstellen, die spezialisierte Funktionen produzieren
23.5) lambda für kurze anonyme Funktionen verwenden
23.5.1) Was sind Lambda-Ausdrücke?
Ein Lambda-Ausdruck erstellt eine kleine, anonyme Funktion—eine Funktion ohne Namen. Lambda-Ausdrücke sind nützlich, wenn Sie für kurze Zeit eine einfache Funktion benötigen und sie nicht formal mit def definieren möchten.
Die Syntax ist:
lambda parameters: expressionDas Lambda nimmt Parameter (wie eine normale Funktion) und gibt das Ergebnis der Auswertung des Ausdrucks zurück. Hier ist ein einfaches Beispiel:
# Normale Funktion
def add(x, y):
return x + y
# Entsprechender Lambda-Ausdruck
add_lambda = lambda x, y: x + y
# Beide funktionieren auf die gleiche Weise
print(add(3, 5)) # Output: 8
print(add_lambda(3, 5)) # Output: 8Lambda-Ausdrücke sind auf einen einzelnen Ausdruck beschränkt—sie können keine Anweisungen wie if, for oder mehrere Codezeilen enthalten. Diese Einschränkung hält sie einfach und fokussiert.
23.5.2) Lambda-Ausdrücke als Argumente
Lambda-Ausdrücke glänzen, wenn Sie eine einfache Funktion als Argument übergeben müssen und keine separate benannte Funktion definieren möchten:
# Schüler nach Note sortieren mit lambda
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 92},
{"name": "Charlie", "grade": 78},
{"name": "Diana", "grade": 95}
]
# Anstatt eine separate Funktion zu definieren:
# def get_grade(student):
# return student["grade"]
# sorted_students = sorted(students, key=get_grade)
# Wir können direkt ein Lambda verwenden:
sorted_students = sorted(students, key=lambda student: student["grade"])
print("Students sorted by grade:")
for student in sorted_students:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95Das ist kürzer, wenn die Funktion einfach ist und nur einmal verwendet wird. Das Lambda lambda student: student["grade"] ist äquivalent zu einer Funktion, die einen Schüler entgegennimmt und seine Note zurückgibt.
23.5.3) Lambda mit mehreren Parametern
Lambda-Ausdrücke können mehrere Parameter annehmen, genau wie normale Funktionen:
# Rechner-Operationen mit lambda
operations = {
'add': lambda x, y: x + y,
'subtract': lambda x, y: x - y,
'multiply': lambda x, y: x * y,
'divide': lambda x, y: x / y if y != 0 else "Error"
}
# Die Lambda-Ausdrücke verwenden
print(operations['add'](10, 5)) # Output: 15
print(operations['multiply'](10, 5)) # Output: 50
print(operations['divide'](10, 0)) # Output: ErrorBeachten Sie, wie wir einen bedingten Ausdruck (x / y if y != 0 else "Error") innerhalb eines Lambda verwenden können, aber keine if-Anweisung (die mehrere Zeilen erfordern würde).
23.5.4) Wann man Lambda vs. benannte Funktionen verwenden sollte
Verwenden Sie Lambda-Ausdrücke, wenn:
- Die Funktion sehr einfach ist (ein Ausdruck)
- Die Funktion nur einmal oder in einem sehr lokalen Kontext verwendet wird
- Das Definieren einer benannten Funktion unnötig ausführlich wäre
Verwenden Sie eine benannte Funktion, wenn:
- Die Funktion komplex ist oder mehrere Anweisungen benötigt
- Die Funktion an mehreren Stellen wiederverwendet wird
- Die Funktion zur Klarheit einen beschreibenden Namen braucht
- Die Funktion eine Docstring benötigt
23.5.5) Lambda-Einschränkungen und Alternativen
Lambda-Ausdrücke haben wichtige Einschränkungen:
# ❌ Das funktioniert nicht – lambda kann keine Anweisungen enthalten
# bad_lambda = lambda x:
# if x > 0:
# return x
# else:
# return -x
# ✅ Verwenden Sie stattdessen einen bedingten Ausdruck
absolute_value = lambda x: x if x > 0 else -x
print(absolute_value(-5)) # Output: 5
print(absolute_value(3)) # Output: 3
# ✅ Für mehrere Operationen verwenden Sie eine normale Funktion
def process_and_double(x):
print(f"Processing: {x}")
return x * 2
result = process_and_double(5) # Output: Processing: 5
print(result) # Output: 10Lambda-Ausdrücke sind Werkzeuge für bestimmte Situationen. Wenn sie Code klarer und prägnanter machen, verwenden Sie sie. Wenn sie Code schwerer verständlich machen, verwenden Sie stattdessen eine normale benannte Funktion.
23.6) map() und filter() mit einfachen Funktionen verwenden
23.6.1) Die Funktion map()
Die Funktion map() wendet eine gegebene function auf jedes Element eines Iterables (wie einer Liste, eines Tupels oder eines Strings) an und gibt einen Iterator mit den Ergebnissen zurück. Es ist eine Möglichkeit, jedes Element in einer Sammlung zu transformieren, ohne eine explizite Schleife zu schreiben.
map(function, iterable, *iterables)Parameter:
function(erforderlich): Eine Funktion, die ein oder mehrere Argumente entgegennimmt, sie verarbeitet und einen Wert zurückgibt. Die Funktion wird für jedes Element in deniterable(s)einmal aufgerufen.iterable(erforderlich): Eine Sequenz (Liste, Tupel, String usw.), deren Elemente an diefunctionübergeben werden.*iterables(optional): Zusätzliche Iterables für einefunctionmit mehreren Argumenten.
Wenn mehrere Iterables bereitgestellt werden, muss function entsprechend viele Argumente akzeptieren
map() stoppt, wenn das kürzeste Iterable erschöpft ist
Rückgabewert:
Ein map-Objekt (Iterator), das die von der function zurückgegebenen Ergebnisse für jedes Eingabeelement enthält.
Wichtig: Das map-Objekt ist ein Iterator, keine Sequenz wie eine list.
# Jede Zahl in einer Liste verdoppeln
numbers = [1, 2, 3, 4, 5]
def double(x):
return x * 2
# double auf jede Zahl anwenden
doubled = map(double, numbers)
result = list(doubled) # map-Objekt (Iterator) in Liste umwandeln
print(result) # Output: [2, 4, 6, 8, 10]23.6.2) map() mit Lambda verwenden
Lambda-Ausdrücke funktionieren perfekt mit map() für einfache Transformationen:
# Temperaturen von Celsius nach Fahrenheit umrechnen
celsius_temps = [0, 10, 20, 30, 40]
fahrenheit_temps = list(map(lambda c: (c * 9/5) + 32, celsius_temps))
print(fahrenheit_temps) # Output: [32.0, 50.0, 68.0, 86.0, 104.0]23.6.3) Die Funktion filter()
Die Funktion filter() wendet eine gegebene function auf jedes Element eines iterable an und gibt einen Iterator zurück, der nur die Elemente enthält, für die die Funktion True zurückgibt. Es ist eine Möglichkeit, Elemente aus einer Sammlung auszuwählen, ohne eine explizite Schleife zu schreiben.
filter(function, iterable)Parameter:
function: Eine Funktion, die ein Argument entgegennimmt, es auswertet undTrueoderFalsezurückgibt. Die Funktion wird für jedes Element imiterableeinmal aufgerufen.iterable: Eine Sequenz (Liste, Tupel, String usw.), deren Elemente von derfunctiongetestet werden.
Rückgabewert:
Ein filter-Objekt (Iterator), das nur die Elemente enthält, für die die function True zurückgegeben hat.
Wichtig: Das filter-Objekt ist ein Iterator, keine Sequenz wie eine Liste.
Beispiel:
# Nur gerade Zahlen behalten
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def is_even(x):
return x % 2 == 0
# is_even auf jede Zahl anwenden und nur die behalten, die True zurückgeben
even_numbers = filter(is_even, numbers)
result = list(even_numbers) # filter-Objekt in Liste umwandeln
print(result) # Output: [2, 4, 6, 8, 10]23.6.4) filter() mit Lambda verwenden
Lambda-Ausdrücke werden häufig mit filter() für prägnantes Filtern verwendet:
# Schüler filtern, die bestanden haben (grade >= 60)
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 55},
{"name": "Charlie", "grade": 92},
{"name": "Diana", "grade": 48},
{"name": "Eve", "grade": 73}
]
passed = list(filter(lambda s: s["grade"] >= 60, students))
print("Students who passed:")
for student in passed:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Charlie: 92
# Eve: 7323.6.5) map() und filter() kombinieren
Sie können map()- und filter()-Operationen verketten, um komplexe Transformationen durchzuführen:
# Quadrate gerader Zahlen erhalten
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Zuerst gerade Zahlen filtern, dann quadrieren
even_numbers = filter(lambda x: x % 2 == 0, numbers)
squared = map(lambda x: x ** 2, even_numbers)
result = list(squared)
print(result) # Output: [4, 16, 36, 64, 100]Visueller Vergleich: map() vs filter()
Wichtige Unterschiede:
map(): Wendet eine Funktion an, um jedes Element zu transformieren → Ausgabe hat gleiche Längefilter(): Testet jedes Element und behält nur die, die bestehen → Ausgabe hat gleiche oder kürzere Länge
In diesem Kapitel haben wir Pythons leistungsstarke funktionale Programmierfunktionen erkundet. Wir haben gelernt, dass Funktionen First-Class-Objekte sind, die wie jeder andere Wert weitergereicht werden können, was flexible und wiederverwendbare Code-Muster ermöglicht. Wir haben entdeckt, wie Funktionen andere Funktionen zurückgeben können, wodurch Closures entstehen, die sich ihre Umgebung merken. Wir haben Lambda-Ausdrücke für prägnante Funktionsdefinitionen erkundet, und wir haben map() und filter() verwendet, um Sammlungen elegant zu verarbeiten.
Diese Konzepte bilden die Grundlage für fortgeschrittene Python-Programmiertechniken. In Kapitel 38 werden wir auf diesem Wissen aufbauen, um Decorators (decorators) zu meistern, eines der elegantesten Features von Python.