Die beliebte Pro­gram­mier­spra­che Python ist eher bekannt für ob­jekt­ori­en­tier­te Pro­gram­mie­rung. Doch Python eignet sich ebenfalls gut für funk­tio­na­le Pro­gram­mie­rung. Hier erfahren Sie, welche Funk­tio­nen es gibt und wie Sie diese einsetzen.

Wodurch zeichnet sich funk­tio­na­le Pro­gram­mie­rung aus?

Unter dem Begriff „funk­tio­na­le Pro­gram­mie­rung“ versteht man einen Pro­gram­mier­stil, der Funk­tio­nen als die grund­le­gen­de Einheit von Code verwendet. Dabei gibt es eine graduelle Abstufung von den rein funk­tio­na­len Sprachen wie Haskell oder Lisp hin zu den Multi-Pa­ra­dig­men-Sprachen wie Python. Die Un­ter­schei­dung, ob eine Sprache funk­tio­na­le Pro­gram­mie­rung un­ter­stützt, ist also nicht schwarz oder weiß.

Damit funk­tio­na­le Pro­gram­mie­rung in einer Sprache möglich ist, muss diese Funk­tio­nen als so­ge­nann­te First-Class Citizens behandeln. Das ist in Python gegeben, denn dort sind Funk­tio­nen Objekte, genau wie Strings, Zahlen und Listen. Funk­tio­nen lassen sich als Parameter an andere Funk­tio­nen übergeben bzw. als Rück­ga­be­wer­te von Funk­tio­nen zu­rück­ge­ben.

Funk­tio­na­le Pro­gram­mie­rung ist de­kla­ra­tiv

Bei Nutzung der de­kla­ra­ti­ven Pro­gram­mie­rung be­schreibt man ein Problem und überlässt die Lö­sungs­fin­dung der Sprach­um­ge­bung. Im Gegensatz dazu beruht der im­pe­ra­ti­ve Ansatz darin, Schritt für Schritt den Weg zur Lösung zu be­schrei­ben. Die funk­tio­na­le Pro­gram­mie­rung zählt zum de­kla­ra­ti­ven Ansatz; mit Python lassen sich beide Ansätze verfolgen.

Be­trach­ten wir ein konkretes Beispiel in Python. Gegeben ist eine Liste von Zahlen. Wir möchten die dazu kor­re­spon­die­ren­den Qua­drat­zah­len berechnen. Wir zeigen zunächst den im­pe­ra­ti­ven Ansatz:

# Calculate squares from list of numbers
def squared(nums):
    # Start with empty list
    squares = []
    # Process each number individually
    for num in nums:
        squares.append(num ** 2)
    return squares

Python un­ter­stützt mit den List Com­pre­hen­si­ons einen de­kla­ra­ti­ven Ansatz, der sich gut mit funk­tio­na­len Techniken kom­bi­nie­ren lässt. Wir erzeugen die Liste der Qua­drat­zah­len ohne explizite Schleife. Der ent­ste­hen­de Code ist deutlich schlanker und kommt ohne Ein­rü­ckun­gen aus:

# Numbers 0–9
nums = range(10)
# Calculate squares using list expression
squares = [num ** 2 for num in nums]
# Show that both methods give the same result
assert squares == squared(nums)

Reine Funk­tio­nen werden gegenüber Pro­ze­du­ren bevorzugt

Eine Pure Function, also „reine Funktion“, ist ver­gleich­bar mit den grund­le­gen­den ma­the­ma­ti­schen Funk­tio­nen. Der Begriff be­zeich­net eine Funktion mit den folgenden Ei­gen­schaf­ten:

  • Die Funktion liefert für dieselben Argumente dasselbe Ergebnis.
  • Die Funktion hat aus­schließ­lich Zugriff auf ihre Argumente.
  • Die Funktion löst keine Side Effects („Ne­ben­ef­fek­te“) aus.

Zu­sam­men­ge­fasst bedeuten diese Ei­gen­schaf­ten, dass sich beim Aufruf einer reinen Funktion das umgebende System nicht verändert. Be­trach­ten wir das klas­si­sche Beispiel der Qua­drat­funk­ti­on f(x) = x * x. Diese lässt sich in Python leicht als reine Funktion umsetzen:

def f(x):
    return x * x
# let's test
assert f(9) == 81

Im Gegensatz zu den reinen Funk­tio­nen stehen die in älteren Sprachen wie Pascal und Basic ver­brei­te­ten Pro­ze­du­ren. Genau wie eine Funktion ist eine Prozedur ein benannter Code-Block, der sich mehrfach aufrufen lässt. Jedoch liefert eine Prozedur keinen Rück­ga­be­wert. Statt­des­sen greifen Pro­ze­du­ren bei Bedarf direkt auf nicht­lo­ka­le Variablen zu und mo­di­fi­zie­ren diese bei Bedarf.

In C und Java werden Pro­ze­du­ren als Funktion mit Rück­ga­be­typ void rea­li­siert. In Python gibt eine Funktion immer einen Wert zurück: Ist keine return-Anweisung vorhanden, wird der spezielle Wert „None“ zu­rück­ge­ge­ben. Wenn wir in Python von Pro­ze­du­ren sprechen, meinen wir also eine Funktion ohne return-Anweisung.

Schauen wir uns ein paar Beispiele für reine und unreine Funk­tio­nen in Python an. Die folgende Funktion ist unrein, da sie bei jedem Aufruf ein anderes Ergebnis liefert:

# Function without arguments
def get_date():
    from datetime import datetime
    return datetime.now()

Die folgende Prozedur ist unrein, da sie auf außerhalb der Funktion de­fi­nier­te Daten zugreift:

# Function using non-local value
name = 'John'
def greetings_from_outside():
    return(f"Greetings from {name}")

Die folgende Funktion ist unrein, da sie beim Aufruf ein mu­tier­ba­res Argument verändert und somit das umgebende System be­ein­flusst:

# Function modifying argument
def greetings_from(person):
    print(f"Greetings from {person['name']}")
    # Changing `person` defined somewhere else
    person['greeted'] = True
    return person
# Let's test
person = {'name': "John"}
# Prints `John`
greetings_from(person)
# Data was changed from inside function
assert person['greeted']

Die folgende Funktion ist rein, da sie für dasselbe Argument dasselbe Ergebnis liefert und keine Ne­ben­ef­fek­te her­vor­ruft:

# Pure function
def squared(num):
    return num * num

Rekursion wird als Al­ter­na­ti­ve zur Iteration genutzt

In der funk­tio­na­len Pro­gram­mie­rung ist die Rekursion das Ge­gen­stück zur Iteration. Eine rekursive Funktion ruft sich selbst wie­der­holt auf, um ein Ergebnis zu erzielen. Damit dies funk­tio­niert, ohne dass die Funktion eine End­los­schlei­fe her­vor­ruft, müssen zwei Be­din­gun­gen erfüllt sein:

  1. Die Rekursion muss bei Erreichen eines Basis-Falls ter­mi­nie­ren.
  2. Beim re­kur­si­ven Durch­lau­fen der Funktion muss eine Ver­klei­ne­rung des Problems erfolgen.

Python un­ter­stützt rekursive Funk­tio­nen. Wir zeigen als berühmtes Beispiel das Berechnen der Fibonacci-Sequenz. Es handelt sich dabei um den so­ge­nann­ten naiven Ansatz; dieser ist für große Werte von n nicht per­for­mant, lässt sich jedoch durch Caching gut op­ti­mie­ren:

def fib(n):
    if n == 0 or n == 1:
        return n
    else:
        return fib(n - 2) + fib(n - 1)

Wie gut eignet sich Python für funk­tio­na­le Pro­gram­mie­rung?

Python ist eine Multi-Pa­ra­dig­men-Sprache, d. h. es lassen sich ver­schie­de­ne Pro­gram­mier­pa­ra­dig­men beim Schreiben von Pro­gram­men verfolgen. Neben der funk­tio­na­len Pro­gram­mie­rung lässt sich ins­be­son­de­re die ob­jekt­ori­en­tier­te Pro­gram­mie­rung in Python pro­blem­los umsetzen.

Python hat viel­fäl­ti­ge Tools für funk­tio­na­le Pro­gram­mie­rung an Bord. Jedoch ist der Umfang im Gegensatz zu rein funk­tio­na­len Sprachen wie Haskell be­schränkt. Der Grad der funk­tio­na­len Pro­gram­mie­rung eines Python-Programms hängt in erster Linie vom Pro­gram­mie­rer bzw. von der Pro­gram­mie­re­rin ab. Wir geben einen Überblick der aus­schlag­ge­ben­den funk­tio­na­len Merkmale von Python.

Funk­tio­nen in Python sind First-Class Citizens

In Python gilt: „Ever­y­thing is an object“, und das ist auch für Funk­tio­nen der Fall. Funk­tio­nen lassen sich überall dort innerhalb der Sprache einsetzen, wo andere Objekte erlaubt sind. Schauen wir uns ein konkretes Beispiel an: Wir möchten einen Ta­schen­rech­ner pro­gram­mie­ren, der ver­schie­de­ne ma­the­ma­ti­sche Ope­ra­tio­nen un­ter­stützt.

Wir zeigen zunächst den im­pe­ra­ti­ven Ansatz. Dieser nutzt die klas­si­schen Werkzeuge der struk­tu­rier­ten Pro­gram­mie­rung wie kon­di­tio­na­le Ver­zwei­gun­gen und Zu­wei­sungs-An­wei­sun­gen:

def calculate(a, b, op='+'):
    if op == '+':
        result = a + b
    elif op == '-':
        result = a - b
    elif op == '*':
        result = a * b
    elif op == '/':
        result = a / b
    return result

Be­trach­ten wir nun einen de­kla­ra­ti­ven Ansatz zur Lösung desselben Problems. Statt der if-Ver­zwei­gung bilden wir die Ope­ra­tio­nen als Python-Dict ab. Dabei verweisen die Ope­ra­ti­ons-Symbole als Schlüssel auf kor­re­spon­die­ren­de Funktions-Objekte, die wir aus dem operator-Modul im­por­tie­ren. Der ent­ste­hen­de Code ist über­sicht­li­cher und kommt ohne Ver­zwei­gung aus:

def calculate(a, b, op='+'):
    # Import operator functions
    import operator
    # Map operation symbols to functions
    operations = {
        '+': operator.add,
        '-': operator.sub,
        '*': operator.mul,
        '/': operator.truediv,
    }
    # Choose operation to carry out
    operation = operations[op]
    # Run operation and return results
    return operation(a, b)

Im Anschluss testen wir unsere de­kla­ra­ti­ve calculate-Funktion. Die assert-An­wei­sun­gen zeigen, dass unser Code funk­tio­niert:

# Let's test
a, b = 42, 51
assert calculate(a, b, '+') == a + b
assert calculate(a, b, '-') == a - b
assert calculate(a, b, '*') == a * b
assert calculate(a, b, '/') == a / b

Lambdas sind anonyme Funk­tio­nen in Python

Neben dem bekannten Weg, Funk­tio­nen in Python per def-Schlüs­sel­wort zu de­fi­nie­ren, kennt die Sprache die sog. „Lambdas“. Das sind kurze, anonyme (sprich: un­be­nann­te) Funk­tio­nen, welchen einen Ausdruck mit Pa­ra­me­tern de­fi­nie­ren. Lambdas lassen sich überall einsetzen, wo eine Funktion erwartet wird, oder per Zuweisung an einen Namen binden:

squared = lambda x: x * x
assert squared(9) == 81

Unter Nutzung von Lambdas können wir unsere calculate-Funktion ver­bes­sern. Anstatt die ver­füg­ba­ren Ope­ra­tio­nen innerhalb der Funktion hart zu kodieren, übergeben wir ein Dict mit Lambda-Funk­tio­nen als Werten. Dies erlaubt uns, später einfach neue Ope­ra­tio­nen hin­zu­zu­fü­gen:

def calculate(a, b, op, ops={}):
    # Get operation from dict and define noop for non-existing key
    operation = ops.get(op, lambda a, b: None)
    return operation(a, b)
# Define operations
operations = {
    '+': lambda a, b: a + b,
    '-': lambda a, b: a - b,
}
# Let's test
a, b, = 42, 51
assert calculate(a, b, '+', operations) == a + b
assert calculate(a, b, '-', operations) == a - b
# Non-existing key handled gracefully
assert calculate(a, b, '**', operations) == None
# Add a new operation
operations['**'] = lambda a, b: a ** b
assert calculate(a, b, '**', operations) == a ** b

Funktion höherer Ordnung in Python

Lambdas werden oft im Zu­sam­men­hang mit Funk­tio­nen höherer Ordnung wie map() und filter() verwendet. So lassen sich die Elemente eines Iterable ohne Einsatz von Schleifen trans­for­mie­ren. Die map()-Funktion nimmt als Parameter eine Funktion und ein Iterable entgegen und führt die Funktion für jedes Element des Iterable aus. Be­trach­ten wir wiederum das Problem, Qua­drat­zah­len zu erzeugen:

nums = [3, 5, 7]
squares = map(lambda x: x ** 2, nums)
assert list(squares) == [9, 25, 49]
Hinweis

Als Funk­tio­nen höherer Ordnung (engl. higher-order functions) werden Funk­tio­nen be­zeich­net, die als Parameter Funk­tio­nen ent­ge­gen­neh­men oder eine Funktion zu­rück­ge­ben.

Mit der filter()-Funktion lassen sich die Elemente eines Iterable filtern. Wir erweitern unser Beispiel, sodass nur gerade Qua­drat­zah­len erzeugt werden:

nums = [1, 2, 3, 4]
squares = list(map(lambda num: num ** 2, nums))
even_squares = filter(lambda square: square % 2 == 0, squares)
assert list(even_squares) == [4, 16]

Iterables, Com­pre­hen­si­ons und Ge­ne­ra­tors

Iterables sind ein Kern­kon­zept von Python. Es handelt sich um eine Abs­trak­ti­on über Kol­lek­tio­nen, deren Elemente sich einzeln ausgeben lassen. Dazu zählen u. a. Strings, Tuples, Listen und Dicts, die allesamt denselben Regeln folgen. Bei­spiels­hal­ber lässt sich der Umfang eines Iterable mit der len()-Funktion abfragen:

name = 'Walter White'
assert len(name) == 12
people = ['Jim', 'Jack', 'John']
assert len(people) == 3

Aufbauend auf den Iterables kommen die Com­pre­hen­si­ons zum Einsatz. Diese eignen sich gut für funk­tio­na­le Pro­gram­mie­rung und haben weit­ge­hend die Nutzung von Lambdas mit map() und filter() abgelöst.

# List comprehension to create first ten squares
squares = [num ** 2 for num in range(10)]

Wie aus rein funk­tio­na­len Sprachen bekannt, bietet Python mit den so­ge­nann­ten Ge­ne­ra­to­ren einen Ansatz für Lazy Eva­lua­ti­on. Das heißt, dass Daten erst beim Zugriff erzeugt werden. Dadurch lässt sich eine Menge Ar­beits­spei­cher sparen. Wir zeigen eine Generator Ex­pres­si­on, die jede Qua­drat­zahl beim Zugriff errechnet:

# Generator expression to create first ten squares
squares = (num ** 2 for num in range(10))

Mit Hilfe der yield-Anweisung lassen sich Lazy-Funk­tio­nen in Python ver­wirk­li­chen. Wir schreiben eine Funktion, die die positiven Zahlen bis zu einem gegebenen Limit liefert:

def N(limit):
    n = 1
    while n <= limit:
        yield n
        n += 1

Welche Al­ter­na­ti­ven zu Python gibt es für funk­tio­na­le Pro­gram­mie­rung?

Die funk­tio­na­le Pro­gram­mie­rung erfreut sich seit langem großer Be­liebt­heit und hat sich als primärer Ge­gen­strom zur ob­jekt­ori­en­tier­ten Pro­gram­mie­rung etabliert. Die Kom­bi­na­ti­on un­ver­än­der­li­cher („immutable“) Da­ten­struk­tu­ren mit reinen Funk­tio­nen führt zu Code, der sich leicht par­al­le­li­sie­ren lässt. Somit ist die funk­tio­na­le Pro­gram­mie­rung besonders in­ter­es­sant für die Trans­for­ma­ti­on von Daten in Daten-Pipelines.

Besonders stark auf­ge­stellt sind rein funk­tio­na­le Sprachen mit starken Typ-Systemen wie Haskell oder der Lisp-Dialekt Clojure. An­de­rer­seits gilt auch Ja­va­Script als im Herzen funk­tio­na­le Sprache. Mit Ty­pe­Script steht eine moderne Al­ter­na­ti­ve mit starker Ty­pi­sie­rung zur Verfügung.

Tipp

Sie wollen online mit Python arbeiten? Dann können Sie Webspace für Ihr Projekt mieten.

Zum Hauptmenü