Lambda, filter, reduce und map


Lambda-Operator

Ring als Symbol der for-Schleife

Wenn es nach Guido van Rossum, dem Autor von Python, gegangen wäre, würde dieses Kapitel in unserem Tutorial fehlen. Guido van Rossum hatte lambda, reduce(), filter() and map() noch nie gemocht und sie bereits 1993 widerwillig in Python aufgenommnen, nachdem er eine Codeerweiterung mit diesen Funktionalitäten von einem, wie er glaubt, Lisp-Hacker erhalten hatte.1 In Python3 sollten sie nach seinem Willen verschwinden. Dies begründete er wie folgt:

Der lambda-Operator bietet eine Möglichkeit anonyme Funktionen, also Funktionen ohne Namen, zu schreiben und zu benutzen. Lambda-Funktionen kommen aus der funktionalen Programmierung und wurden insbesondere durch die Programmiersprache Lisp besonders bekannt. Sie können eine beliebe Anzahl von Parametern haben, führen einen Ausdruck aus und liefern den Wert dieses Ausdrucks als Rückgabewert zurück.

Anonyme Funktionen sind insbesondere bei der Anwendung der map-, filter- und reduce-Funktionen besonders vorteilhaft.

Allgemeine Syntax einer Lambda-Funktion:
lambda Argumentenliste: Ausdruck

Die Argumentenliste besteht aus einer durch Kommata getrennten Liste von Argumenten, und der 'Ausdruck' ist ein Ausdruck, der diese Argumente benutzt.
Schauen wir uns ein einfaches Beispiel einer lambda-Funktion an.
lambda x: x + 42
Bei dem obigen Beispiel handelt es sich um eine Funktion mit einem Argument "x", die die Summe von x und 42 zurückgibt. Doch wie können wir die obige Funktion benutzen? Sie ist namenlos!
>>> (lambda x: x + 42)(3)
45
>>> y = (lambda x: x + 42)(3) - 100
>>> print(y)
-55
>>> for i in range(10):
...     (lambda x: x + 42)(i)
... 
42
43
44
45
46
47
48
49
50
51
Natürlich kann man mit obigem Code viele beeindrucken, aber darin liegt natürlich nicht der Sinn von Python. Ein wichtiges Ziel eines guten Python-Programmes sollte immer die leichte Lesbarkeit sein. Obigen Code könnte man ebenso gut wie folgt schreiben:
>>> for i in range(10):
...     print(42 + i)
... 
42
43
44
45
46
47
48
49
50
51
Eine andere Möglichkeit besteht darin, den lambda-Ausdruck einer Variablen zuzweisen. Mit diesem Namen können wir unsere Funktion jetzt wie eine "gewöhnliche" Funktion f benutzen:
>>> f42 = lambda x: x + 42
>>> f42(4)
46
Aber dazu brauchten wir nicht die lambda-Notation. Wir hätten dies auch mit einer normalen Funktionsdefinition bewerkstelligen können:
>>> def f42(x):
...     return x + 42
... 
>>> f42(4)
46
Nun kommen wir endlich zu einer sinnvollen Anwendung der lambda-Notation. Wir schreiben eine Funktion mit dem Namen "anwenden", die eine Funktion als erstes Argument und eine Liste als zweites Argument erwartet. Die Funktion "anwenden" wendet auf jedes Element der übergebenen Liste die als erstes Argument übergebene Funktion an:
>>> def anwenden(f,liste):
...     ergebnis = []
...     for element in liste:
...         ergebnis.append(f(element))
...     return ergebnis
Wir können nun die Funktion "anwenden" mit unserer f42-Funktion und der Liste der Zahlen von 0 bis 9 aufrufen:
>>> anwenden(f42,range(10))
[42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
Wir haben oben den Funktionsnamen als Referenz auf unsere Funktion übergeben. Um diese Funktion zu benutzen, hatten wir zuerst eine Funktion mit dem --- hässlichen --- Namen f42 einführen müssen. Die Funktion f42 ist eine "Wegwerffunktion", die wir nur einmal bei dem Funktionsaufruf von "anwenden" benötigen. Sie können sich leicht vorstellen, dass wir gegebenenfalls auch ähnliche Funktionen wie f43, f44 und so weiter benötigen könnten. Aus diesem Grund wäre es natürlich bedeutend eleganter, wenn wir diese Funktionen direkt an unsere Funktion "anwenden" übergeben könnten, also ohne den Umweg mit der Namensgebung. Dies ist mit der lambda-Notation möglich:
>>> anwenden(lambda x: x + 42,range(10))
[42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
Auch solche Anwendungen sind jetzt möglich:
>>> for i in [17, 22,42]:
...      anwenden(lambda x: x + i, range(10))
... 
[17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
[22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
[42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
In obigem Beispiel haben wir mit Hilfe des Schleifenparameters i drei verschiedene Funktionen kreiert.

map-Funktion

Nachdem wir uns im vorigen Kapitel intensiv mit der Funktion "anwenden" beschäftigt hatten, stellt die von Python zur Verfügung gestellte Funktion "map" kein Problem dar. Im Prinzip entspricht "map" unserer Funktion "anwenden". Map ist eine Funktion mit zwei Argumenten:
r = map(func, seq)
Das erste Argument func ist eine Funktion und das zweite eine Sequenz (z.B. eine Liste oder Tupel) seq. map wendet die Funktion func auf alle Elemente von seq an und schreibt die Ergebnisse in ein map object, also ein Iterator. Rufen wir
map(lambda x: x + 42, range(10))
entspricht dies fast unserem Aufruf
anwenden(lambda x: x + 42, range(10))
Damit es ist völlig gleich ist, müssen wir lediglich das map-Objekt noch in eine Liste wandeln:
>>> list(map(lambda x: x + 42, range(10)))
[42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
>>> anwenden(lambda x: x + 42, range(10))
[42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

In einem weiteren Beispiel wollen wir nun zeigen, welchen großen Vorteil diese Kombination aus lambda und map-Funktion mit sich bringt. Nehmen wir an, dass wir Listen mit Temperaturwerten in Grad Celcius und Grad Fahrenheit haben. Wir möchten diese wechselseitig in die jeweilig andere Temperaturskala wandeln. Eine Temperatur C in Grad Celsius lässt sich mittels der Formel "9/5 * C + 32" in Grad Fahrenheit wandeln und eine Temperatur F in Grad Fahrenheit lässt sich mittels der Formel "5/9 * (F - 32)" in Grad Celsius wandeln. Für das folgende Beispiel "vergessen" wir nun "lambda" und "map" und beschränken uns auf eine "konventionelle" Programmierung:
def fahrenheit(T):
    return ((9.0 / 5) * T + 32)
def celsius(T):
    return (5.0 / 9) * ( T - 32 )
temp = (36.5, 37, 37.5,39)

def Fahrenheit2Celsius(F_liste):
    erg = []
    for F in F_liste:
        erg.append(celsius(F))
    return erg

def Celsius2Fahrenheit(C_liste):
    erg = []
    for C in C_liste:
        erg.append(fahrenheit(C))
    return erg

F_liste = Celsius2Fahrenheit(temp)
print(F_liste)

C_liste = Fahrenheit2Celsius(F_liste)
print(C_liste)
Unter Benutzung von lambda und map schrumpft unser obiges Codebeispiel in beachtlicher Weise:
temp = (36.5, 37, 37.5,39)

F_liste = list(map(lambda C: (5.0 / 9) * ( C - 32 ), temp))
print(F_liste)

C_liste = map(lambda F: (9.0 / 5) * F + 32, F_liste)
print(list(C_liste))
map kann auch gleichzeitig auf mehrere Listen angewendet werden. Dann werden die Argumente entsprechend ihrer Position und der Reihenfolge der Listenargumente entsprechend mit den Werten aus den Listen versorgt.
>>> a = [1,2,3,4]
>>> b = [17,12,11,10]
>>> c = [-1,-4,5,9]
>>> list(map(lambda x,y:x+y, a,b))
[18, 14, 14, 14]
>>> list(map(lambda x,y,z:x+y+z, a,b,c))
[17, 10, 19, 23]
>>> list(map(lambda x,y,z : 2.5*x + 2*y - z, a,b,c))
[37.5, 33.0, 24.5, 21.0]
>>> 
Wir sehen in dem obigen Beispiel, dass der Parameter x seine Werte aus der Liste a, der Parameter y seine Werte aus der Liste b und der Parameter z seine Werte aus der Liste c beziehen.

Filtern

Die Funktion filter(funktion, liste) bietet eine elegante Möglichkeit diejenigen Elemente aus der Liste liste herauszufiltern, für die die Funktion funktion True liefert.
Die Funktion
filter(f,iter) 
benötigt als erstes Argument eine Funktion f, die Wahrheitswerte liefert. Diese Funktion wird dann auf jedes Argument des Objektes "iter" angewendet. "iter" ist entweder ein sequentieller Datentyp, wie beispielsweise eine Liste oder ein Tupel, oder es ist ein iterierbares Objekt. Liefert f True für ein x, dann wird x in der Ergebnisliste übernommen, ansonsten wird x nicht übernommen.
>>> fibonacci = [0,1,1,2,3,5,8,13,21,34,55]
>>> odd_numbers = list(filter(lambda x: x % 2, fibonacci))
>>> print(odd_numbers)
[1, 1, 3, 5, 13, 21, 55]
>>> even_numbers = list(filter(lambda x: x % 2 == 0, fibonacci))
>>> print(even_numbers)
[0, 2, 8, 34]
>>> 
>>> 
>>> # or alternatively:
... 
>>> even_numbers = list(filter(lambda x: x % 2 -1, fibonacci))
>>> print(even_numbers)
[0, 2, 8, 34]
>>> 

reduce-Funktion

Wie wir bereits im Anfang dieses Kapitels erwähnt haben, mag Guido van Rossum keines der hier eingeführten Konstrukte. reduce hasst er am meisten, wie wir in seinem Posting vom 10. März 2005 erfahren können.2

Aber mit reduce() konnte er sich durchsetzen. reduce() wurde in das Modul functools verbannt, gehört also nicht mehr zum Kern der Sprache.

Die Funktion
reduce(func, seq) 
wendet die Funktion func() fortlaufend auf eine Sequenz seq an und liefert einen einzelnen Wert zurück.

Falls seq = [ s1, s2, s3, ... , sn ] ist, funktioniert der Aufruf reduce(func, seq) wie folgt:

Für den Fall n = 4 können wir die vorige Erklärung auch wie folgt illustrieren:

Reduce

Das folgende Beispiel zeigt die Arbeitsweise von reduce() an einem einfachen Beispiel. Um mit reduce zu arbeiten, müssen wir in Python3 das Modul functools importieren. Das ist der wesentliche Unterschied zu früheren Python-Versionen wie zum Beispiel Python 2.7:
>>> import functools
>>> functools.reduce(lambda x,y: x+y, [47,11,42,13])
113
>>> 
Im folgenden Diagram sind die Ergebniswerte dargestellt:

Veranschulichung von Reduce

Übungen

  1. In einer Buchhandlung findet sich in einem Abrechnungsprogramm in Python eine Liste mit Unterlisten mit folgendem Aufbau:
    Bestell-Nummer Buchtitel und Autor Anzahl Einzelpreis
    34587 Learning Python, Mark Lutz 4 40.95
    98762 Programming Python, Mark Lutz 5 56.80
    77226 Head First Python, Paul Barry 3 32.95
    Schreibe ein Programm, das als Ergebnis eine Liste mit Zweier-Tupel liefert. Jedes Tupel besteht aus der Bestellnummer und dem Produkt aus der Anzahl und dem Einzelpreis. Das Produkt soll jedoch um 10,- € erhöht werden, wenn der Bestellwert unter 100,00 € liegt.
    Schreibe ein Python-Programm unter Benutzung von lambda und map.
  2. Situation wie in voriger Aufgabe, aber jetzt sehen die Unterlisten wie folgt aus: [Bestellnummer, (Artikel-Nr, Anzahl, Einzelpreis), ... (Artikel-Nr, Anzahl, Einzelpreis) ]. Schreibe wieder ein Programm, was eine Liste mit 2-Tupel (Bestellnummer, Gesamtpreis) liefert.

Lösungen zu den Übungen

  1. orders = [ ["34587","Learning Python, Mark Lutz", 4, 40.95], 
    	   ["98762","Programming Python, Mark Lutz", 5, 56.80], 
               ["77226","Head First Python, Paul Barry",3,32.95]]
    
    min_order = 100
    invoice_totals = list(map(lambda x: x if x[1] >= min_order else (x[0], x[1] + 10) , 
    			  map(lambda x: (x[0],x[2] * x[3]), orders)))
    
    print(invoice_totals)
    
  2. from functools import reduce
    
    orders = [ ["34587",("5464", 4, 9.99), ("8274",18,12.99), ("9744", 9, 44.95)], 
    	   ["34588",("5464", 9, 9.99), ("9744", 9, 44.95)],
    	   ["34588",("5464", 9, 9.99)],
               ["34587",("8732", 7, 11.99), ("7733",11,18.99), ("9710", 5, 39.95)] ]
    
    min_order = 100
    invoice_totals = list(map(lambda x: [x[0]] + list(map(lambda y: y[1]*y[2], x[1:])), orders))
    invoice_totals = list(map(lambda x: [x[0]] + [reduce(lambda a,b: a + b, x[1:])], invoice_totals))
    invoice_totals = list(map(lambda x: x if x[1] >= min_order else (x[0], x[1] + 10), invoice_totals))
    
    print (invoice_totals)
    
    





Fußnoten



1 Guido van van Rossum: All Things Pythonic: The fate of reduce() in Python 3000, March 10, 2005

2 dto., wörtlich "So now reduce(). This is actually the one I've always hated most, because, apart from a few examples involving + or *, almost every time I see a reduce() call with a non-trivial function argument, I need to grab pen and paper to diagram what's actually being fed into that function before I understand what the reduce() is supposed to do."