Python Functional Programming

Die beliebte Programmiersprache Python ist eher bekannt für objektorientierte Programmierung. Doch Python eignet sich ebenfalls gut für funktionale Programmierung. Hier erfahren Sie, welche Funktionen es gibt und wie Sie diese einsetzen.

Wodurch zeichnet sich funktionale Programmierung aus?

Unter dem Begriff „funktionale Programmierung“ versteht man einen Programmierstil, der Funktionen als die grundlegende Einheit von Code verwendet. Dabei gibt es eine graduelle Abstufung von den rein funktionalen Sprachen wie Haskell oder Lisp hin zu den Multi-Paradigmen-Sprachen wie Python. Die Unterscheidung, ob eine Sprache funktionale Programmierung unterstützt, ist also nicht schwarz oder weiß.

Damit funktionale Programmierung in einer Sprache möglich ist, muss diese Funktionen als sogenannte First-Class Citizens behandeln. Das ist in Python gegeben, denn dort sind Funktionen Objekte, genau wie Strings, Zahlen und Listen. Funktionen lassen sich als Parameter an andere Funktionen übergeben bzw. als Rückgabewerte von Funktionen zurückgeben.

Funktionale Programmierung ist deklarativ

Bei Nutzung der deklarativen Programmierung beschreibt man ein Problem und überlässt die Lösungsfindung der Sprachumgebung. Im Gegensatz dazu beruht der imperative Ansatz darin, Schritt für Schritt den Weg zur Lösung zu beschreiben. Die funktionale Programmierung zählt zum deklarativen Ansatz; mit Python lassen sich beide Ansätze verfolgen.

Betrachten wir ein konkretes Beispiel in Python. Gegeben ist eine Liste von Zahlen. Wir möchten die dazu korrespondierenden Quadratzahlen berechnen. Wir zeigen zunächst den imperativen 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 unterstützt mit den List Comprehensions einen deklarativen Ansatz, der sich gut mit funktionalen Techniken kombinieren lässt. Wir erzeugen die Liste der Quadratzahlen ohne explizite Schleife. Der entstehende Code ist deutlich schlanker und kommt ohne Einrückungen 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 Funktionen werden gegenüber Prozeduren bevorzugt

Eine Pure Function, also „reine Funktion“, ist vergleichbar mit den grundlegenden mathematischen Funktionen. Der Begriff bezeichnet eine Funktion mit den folgenden Eigenschaften:

  • Die Funktion liefert für dieselben Argumente dasselbe Ergebnis.
  • Die Funktion hat ausschließlich Zugriff auf ihre Argumente.
  • Die Funktion löst keine Side Effects („Nebeneffekte“) aus.

Zusammengefasst bedeuten diese Eigenschaften, dass sich beim Aufruf einer reinen Funktion das umgebende System nicht verändert. Betrachten wir das klassische Beispiel der Quadratfunktion 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 Funktionen stehen die in älteren Sprachen wie Pascal und Basic verbreiteten Prozeduren. Genau wie eine Funktion ist eine Prozedur ein benannter Code-Block, der sich mehrfach aufrufen lässt. Jedoch liefert eine Prozedur keinen Rückgabewert. Stattdessen greifen Prozeduren bei Bedarf direkt auf nichtlokale Variablen zu und modifizieren diese bei Bedarf.

In C und Java werden Prozeduren als Funktion mit Rückgabetyp void realisiert. In Python gibt eine Funktion immer einen Wert zurück: Ist keine return-Anweisung vorhanden, wird der spezielle Wert „None“ zurückgegeben. Wenn wir in Python von Prozeduren sprechen, meinen wir also eine Funktion ohne return-Anweisung.

Schauen wir uns ein paar Beispiele für reine und unreine Funktionen 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 definierte 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 mutierbares Argument verändert und somit das umgebende System beeinflusst:

# 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 Nebeneffekte hervorruft:

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

Rekursion wird als Alternative zur Iteration genutzt

In der funktionalen Programmierung ist die Rekursion das Gegenstück zur Iteration. Eine rekursive Funktion ruft sich selbst wiederholt auf, um ein Ergebnis zu erzielen. Damit dies funktioniert, ohne dass die Funktion eine Endlosschleife hervorruft, müssen zwei Bedingungen erfüllt sein:

  1. Die Rekursion muss bei Erreichen eines Basis-Falls terminieren.
  2. Beim rekursiven Durchlaufen der Funktion muss eine Verkleinerung des Problems erfolgen.

Python unterstützt rekursive Funktionen. Wir zeigen als berühmtes Beispiel das Berechnen der Fibonacci-Sequenz. Es handelt sich dabei um den sogenannten naiven Ansatz; dieser ist für große Werte von n nicht performant, lässt sich jedoch durch Caching gut optimieren:

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 funktionale Programmierung?

Python ist eine Multi-Paradigmen-Sprache, d. h. es lassen sich verschiedene Programmierparadigmen beim Schreiben von Programmen verfolgen. Neben der funktionalen Programmierung lässt sich insbesondere die objektorientierte Programmierung in Python problemlos umsetzen.

Python hat vielfältige Tools für funktionale Programmierung an Bord. Jedoch ist der Umfang im Gegensatz zu rein funktionalen Sprachen wie Haskell beschränkt. Der Grad der funktionalen Programmierung eines Python-Programms hängt in erster Linie vom Programmierer bzw. von der Programmiererin ab. Wir geben einen Überblick der ausschlaggebenden funktionalen Merkmale von Python.

Funktionen in Python sind First-Class Citizens

In Python gilt: „Everything is an object“, und das ist auch für Funktionen der Fall. Funktionen lassen sich überall dort innerhalb der Sprache einsetzen, wo andere Objekte erlaubt sind. Schauen wir uns ein konkretes Beispiel an: Wir möchten einen Taschenrechner programmieren, der verschiedene mathematische Operationen unterstützt.

Wir zeigen zunächst den imperativen Ansatz. Dieser nutzt die klassischen Werkzeuge der strukturierten Programmierung wie konditionale Verzweigungen und Zuweisungs-Anweisungen:

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

Betrachten wir nun einen deklarativen Ansatz zur Lösung desselben Problems. Statt der if-Verzweigung bilden wir die Operationen als Python-Dict ab. Dabei verweisen die Operations-Symbole als Schlüssel auf korrespondierende Funktions-Objekte, die wir aus dem operator-Modul importieren. Der entstehende Code ist übersichtlicher und kommt ohne Verzweigung 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 deklarative calculate-Funktion. Die assert-Anweisungen zeigen, dass unser Code funktioniert:

# 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 Funktionen in Python

Neben dem bekannten Weg, Funktionen in Python per def-Schlüsselwort zu definieren, kennt die Sprache die sog. „Lambdas“. Das sind kurze, anonyme (sprich: unbenannte) Funktionen, welchen einen Ausdruck mit Parametern definieren. 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 verbessern. Anstatt die verfügbaren Operationen innerhalb der Funktion hart zu kodieren, übergeben wir ein Dict mit Lambda-Funktionen als Werten. Dies erlaubt uns, später einfach neue Operationen hinzuzufü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 Zusammenhang mit Funktionen höherer Ordnung wie map() und filter() verwendet. So lassen sich die Elemente eines Iterable ohne Einsatz von Schleifen transformieren. Die map()-Funktion nimmt als Parameter eine Funktion und ein Iterable entgegen und führt die Funktion für jedes Element des Iterable aus. Betrachten wir wiederum das Problem, Quadratzahlen zu erzeugen:

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

Als Funktionen höherer Ordnung (engl. higher-order functions) werden Funktionen bezeichnet, die als Parameter Funktionen entgegennehmen oder eine Funktion zurückgeben.

Mit der filter()-Funktion lassen sich die Elemente eines Iterable filtern. Wir erweitern unser Beispiel, sodass nur gerade Quadratzahlen 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, Comprehensions und Generators

Iterables sind ein Kernkonzept von Python. Es handelt sich um eine Abstraktion über Kollektionen, deren Elemente sich einzeln ausgeben lassen. Dazu zählen u. a. Strings, Tuples, Listen und Dicts, die allesamt denselben Regeln folgen. Beispielshalber 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 Comprehensions zum Einsatz. Diese eignen sich gut für funktionale Programmierung und haben weitgehend 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 funktionalen Sprachen bekannt, bietet Python mit den sogenannten Generatoren einen Ansatz für Lazy Evaluation. Das heißt, dass Daten erst beim Zugriff erzeugt werden. Dadurch lässt sich eine Menge Arbeitsspeicher sparen. Wir zeigen eine Generator Expression, die jede Quadratzahl 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-Funktionen in Python verwirklichen. 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 Alternativen zu Python gibt es für funktionale Programmierung?

Die funktionale Programmierung erfreut sich seit langem großer Beliebtheit und hat sich als primärer Gegenstrom zur objektorientierten Programmierung etabliert. Die Kombination unveränderlicher („immutable“) Datenstrukturen mit reinen Funktionen führt zu Code, der sich leicht parallelisieren lässt. Somit ist die funktionale Programmierung besonders interessant für die Transformation von Daten in Daten-Pipelines.

Besonders stark aufgestellt sind rein funktionale Sprachen mit starken Typ-Systemen wie Haskell oder der Lisp-Dialekt Clojure. Andererseits gilt auch JavaScript als im Herzen funktionale Sprache. Mit TypeScript steht eine moderne Alternative mit starker Typisierung zur Verfügung.

Tipp

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