NB_01_Funktionale_Programmierung

(c) 2019/2020 Hochschule Augsburg - Fakultät für Informatik - Prof.Dr.Nik Klever

Funktionale Programmierung

Programmiersprachen unterstützen die Problemlösung auf unterschiedliche Weise:

  • Die meisten Programmiersprachen sind prozedural: Programme sind eine Reihe von Anweisungen, die dem Computer mitteilen, was mit der Eingabe von Programmen gemacht werden soll. C, Pascal und auch Unix-Shells sind prozedurale Sprachen.
  • In deklarativen Sprachen wird eine Spezifikation geschrieben, die das zu lösende Problem beschreibt, und die Implementierung der Sprache übersetzt dann, wie die Berechnung effizient durchführt wird. SQL ist die am häufigsten verwendete deklarative Sprache. Eine SQL-Abfrage beschreibt den Datensatz, der abgerufen werden soll, und die SQL-Engine entscheidet, welche Tabellen oder Indizes verwendet werden, welche Unterabschnitte zuerst ausgeführt werden sollen usw.
  • Objektorientierte Programmiersprachen manipulieren Sammlungen von Objekten. Objekte haben interne Zustände und unterstützen Methoden, die diese internen Zustände in irgendeiner Weise abfragen oder ändern. Smalltalk und Java sind objektorientierte Sprachen. C++ und Python sind Sprachen, die eine objektorientierte Programmierung unterstützen, aber die Verwendung von objektorientierten Methoden nicht erzwingen.
  • Funktionale Programmierung zerlegt ein Problem in eine Reihe von Funktionen. Idealerweise haben Funktionen nur Eingaben (als Parameter) und produzieren Ausgaben (als Rückgabewerte) und haben keinen internen Zustand, der die Ausgabe beeinflusst, der für eine gegebene Eingabe erzeugt wird. Zu den bekannten funktionalen Sprachen gehören die ML-Familie (Standard ML, OCaml und andere Varianten) und Haskell.

Lambda Ausdrücke

Mit dem Lambda-Schlüsselwort können kleine anonyme Funktionen angelegt werden. Die folgende Funktion gibt die Summe ihrer beiden Argumente zurück: lambda a, b: a + b. Lambda-Funktionen können überall dort eingesetzt werden, wo Funktionsobjekte benötigt werden. Sie sind syntaktisch auf einen einzigen Ausdruck beschränkt. Semantisch sind sie nur syntaktischer Zucker für eine normale Funktionsdefinition. Wie verschachtelte Funktionsdefinitionen können Lambda-Funktionen auf Variablen aus dem umgebenden Bereich verweisen:

In [2]:
def make_incrementor(n):
    return lambda x: x + n

f = make_incrementor(42)
In [3]:
x=f(0)
print(x)
42
In [4]:
y=f(1)
print(y)
43

Das obige Beispiel verwendet einen Lambda-Ausdruck, um eine Funktion zurückzugeben. Eine weitere Verwendung ist, eine Lambda-Ausdruck als Argument zu übergeben:

In [5]:
pairs = [(1, 'eins'), (2, 'zwei'), (3, 'drei'), (4, 'vier')]
pairs.sort(key=lambda pair: pair[1])
print(pairs)
[(3, 'drei'), (1, 'eins'), (4, 'vier'), (2, 'zwei')]

Builtin-Funktionen

Schauen wir uns detaillierter Builtin-Funktionen an, die oft mit Iteratoren verwendet werden. Zwei von Pythons Builtin-Funktionen, map() und filter() duplizieren die Features von Generatorausdrücken:

map

map(f, iterA, iterB, ...) gibt einen Iterator über eine Sequenz zurück:

f(iterA[0], iterB[0], ...), f(iterA[1], iterB[1], ...), f(iterA[2], iterB[2], ...), ....

Wichtig bei map ist, dass es genau soviele Iterator-Objekte geben muss, wie die Funktion f Argumente benötigt !

In [6]:
# Funktionalität von map()
Out[6]:
g node0 Funktion "f" f(a,b,...) node1 Liste "A" 0 1 2 ... node0:f0->node1:f0 node0:f0->node1:f1 node0:f0->node1:f2 node2 Liste "B" 0 1 2 ... node0:f0->node2:f0 node0:f0->node2:f1 node0:f0->node2:f2 node4 map Liste f(A°,B°,...) f(A¹,B¹,...) f(A²,B²,...) ... node1:f0->node4:f0 node1:f1->node4:f1 node1:f2->node4:f2 node2:f0->node4:f0 node2:f1->node4:f1 node2:f2->node4:f2 node3 Liste "..." ...
In [7]:
def upper(s):
    return s.upper()

x=list(map(upper, ['sentence', 'fragment']))
print(x)
['SENTENCE', 'FRAGMENT']

Mit einer List Comprehension kann natürlich der gleiche Effekt erzielt werden:

In [8]:
y=[upper(s) for s in ['sentence', 'fragment']]
print(y)
['SENTENCE', 'FRAGMENT']
In [9]:
%%Mooc MoocStringAssessment
Out[9]:

map - Beispiel 1

Es ist das folgenden Dictionary pairs und die Liste order gegeben::

pairs = {'zwei':"zweites Element", 'vier':"viertes Element", 'eins':"erstes Element", 'drei':"drittes Element"}
order = ["eins","zwei","drei","vier"]

print(dict(map(lambda a: (a,pairs[a]),order)))

Welches Ergebnis zeigt dann die obige print-Ausgabe ?



In [10]:
%%Mooc MoocStringAssessment
Out[10]:

map - Beispiel 2

Es ist das folgenden Dictionary pairs und die Liste order gegeben::

pairs = {'zwei':"zweites Element", 'vier':"viertes Element", 'eins':"erstes Element", 'drei':"drittes Element"}
order = ["eins","zwei","drei","vier"]

print(dict(map(lambda a,b: (a,b),order,pairs.values())))

Welches Ergebnis zeigt dann die obige print-Ausgabe ?



filter

filter(predicat, iter) gibt einen Iterator über alle Elemente einer Sequenz zurück, die eine bestimmte Bedingung predicat erfüllen. predicat ist eine Funktion, die den Wahrheitswert einer Bedingung zurückgibt; um ein predicat mit filter() verwenden zu können. muss es einen einzigen booleschen Wert zurückliefern.

In [11]:
def is_even(x):
    return (x % 2) == 0

x=list(filter(is_even, range(10)))
print(x)
[0, 2, 4, 6, 8]

Analog wie bei map kann mit einer List Comprehension der gleiche Effekt erzielt werden:

In [12]:
y=list(x for x in range(10) if is_even(x))
print(y)
[0, 2, 4, 6, 8]

enumerate

enumerate(iter) zählt die Elemente in dem iterierbaren iter-Objekt, wobei ein Tupel mt 2 Elementen, dem Zählwert sowie jedem Element zurückgegeben werden.

In [13]:
for item in enumerate(['subject', 'verb', 'object']):
    print(item)
(0, 'subject')
(1, 'verb')
(2, 'object')

enumerate() wird häufig beim Durchlaufen einer Liste und der Verwendung der Indizes, wenn bestimmte Bedingungen erfüllt sind:

In [14]:
f = """Dies ist ein mehrzeiliger
Text mit einer

Leerzeile dazwischen""".split("\n")

for i, line in enumerate(f):
    if line.strip() == '':
        print('Leerzeile in Zeile #%i' % i)
Leerzeile in Zeile #2
In [15]:
%%Mooc MoocStringAssessment
Out[15]:

enumerate - Beispiel

Es ist die folgende Liste elemente gegeben:

elemente = ["zweites Element", "viertes Element", "erstes Element", "drittes Element"]

print(dict(enumerate(elemente)))

Welches Ergebnis zeigt dann die obige print-Ausgabe ?



sorted

sorted(iterable, key=None, reverse=False) sammelt alle Elemente des iterable-Objekts in eine Liste, sortiert die Liste und gibt das sortierte Ergebnis zurück. Die Schlüssel- und Reverse-Argumente werden an die sort()-Methode der konstruierten Liste übergeben.

In [16]:
import random
# Generate 8 random numbers between [0, 10000)
rand_list = random.sample(range(10000), 8)
print(rand_list)
[2189, 1269, 3549, 9195, 9522, 2649, 802, 1833]
In [17]:
x=sorted(rand_list)
print(x)
[802, 1269, 1833, 2189, 2649, 3549, 9195, 9522]
In [18]:
y=sorted(rand_list, reverse=True)  
print(y)
[9522, 9195, 3549, 2649, 2189, 1833, 1269, 802]

(Für eine ausführlichere Beschreibung der Sortierung siehe die Sorting HOWTO).

any und all

Die Builtin-Funktionen any(iter) und all(iter) betrachten die Wahrheitswerte der Elemente des iterierbaren Objektes iterable. any() gibt True zurück, wenn ein Element in iterable ein wahrer Wert ist und all() gibt True zurück, wenn alle Elemente wahr sind:

In [19]:
x=any([0,1,0])
print(x)
True
In [20]:
y=any([0,0,0])
print(y)
False
In [21]:
z=any([1,1,1])
print(z)
True
In [22]:
x=all([0,1,0])
print(x)
False
In [23]:
y=all([0,0,0])
print(y)
False
In [24]:
z=all([1,1,1])
print(z)
True

zip

zip(iterA, iterB, ...) nimmt ein Element aus jedem iterierbaren Objekt iterA, iterB, etc. und gibt sie in einem entsprechenden Tupel zurück:

In [25]:
x=zip(['a', 'b', 'c'], (1, 2, 3))
print(list(x))
[('a', 1), ('b', 2), ('c', 3)]

zip konstruiert keine Liste im Memory und geht nicht alle Iterator-Objekte iterA, iterB, etc. bis zu deren Ende durch, stattdessen werden die Tupel nur konstruiert und zurückgegeben, wenn sie angefordert werden (z.B. über list() - der Fachbegriff für dieses Verhalten ist "faule Berechnung" (lazy evaluation))

Die Funktion zip wird üblicherweise mit Iterator-Objekte der gleichen Länge verwendet. Wenn die Iterator-Objekte verschieden lang sind, wird das resultierende Iterator-Objekt die gleiche Länge wie das kürzeste Iterator-Objekt haben:

In [26]:
x=zip(['a', 'b'], (1, 2, 3))
print(list(x))
[('a', 1), ('b', 2)]

Man sollte unterschiedliche Längen der Iterator-Objekte vermeiden, da Elemente aus den längeren Iteratoren herausgenommen und gelöscht werden kann. Dies bedeutet, dass an dieser Stelle die Iteration beendet werden sollte um nicht in Gefahr zu laufen, ein gelöschtes Element zu überspringen.

In [27]:
%%Mooc MoocStringAssessment
Out[27]:

zip - Beispiel

Es ist die folgende Liste elemente gegeben:

elemente = ["zweites Element", "viertes Element", "erstes Element", "drittes Element"]
order = ["eins","zwei","drei","vier"]

print(dict(zip(order,elemente)))

Welches Ergebnis zeigt dann die obige print-Ausgabe ?



In [28]:
%%Mooc MoocStringAssessment
Out[28]:

zip mit range als Alternative für enumerate

Testen sie ihr Ergebnis an dem folgenden Code und dem Ergebnis des enumerate-Beispiels von oben

elemente = ["zweites Element", "viertes Element", "erstes Element", "drittes Element"]

print(dict(enumerate(elemente)))

Geben sie eine aus der zip-Funktion und der range-Funktion bestehende Alternative für die enumerate-Funktion an



In [29]:
%%Mooc Video
Out[29]:

Weitere Literatur

In [30]:
%%Mooc WebReference

Functional Programming HOWTO

https://docs.python.org/3/howto/functional.html

Hinweis: Grundlagen und Einführung in die funktionale Programmierung von Python

In [31]:
%%Mooc WebReference

Sorting HOWTO

https://docs.python.org/3/howto/sorting.html

Hinweis: Grundlagen und Einführung in die Sortierung von Python