41. Factory Functions und Anonyme Funktionen

In Word hatten wir die Funktion newSymbol verwendet, die einen String nimmt und je nach Inhalt ein Terminal oder NonTerminal zurückgibt. Mit regulären Ausdrücken kann die Funktion so aussehen:

def newSymbol(sym):
  mat = re.match('"([^"]*)"$', sym)
  if mat:
    return Terminal(mat.group(1))
  return NonTerminal(sym)

Eine Funktion, die wenig anderes macht, als ein Objekt zu erzeugen, nennt man gern factory function.

In Python sind auch Funktionen nur Objekte. Man kann Factory Functions schreiben, die Funktionen zurückgeben. Besonders praktisch dabei sind anonyme Funktionen.

>>> def makeIncFun(inc):
...     return lambda a, inc=inc:a+inc
...
>>> i1,i2 = makeIncFun(1), makeIncFun(3)
>>> i1(3),i2(3)
(4, 6)

Anonyme Funktionen fangen also mit einem lambda an, dann kommen die (formalen) Argumente, dann ein Doppelpunkt und schließlich ein Ausdruck, der als Ergebnis der Funktion zurückkommt. Das inc=inc hat nichts mit lambda zu tun, es ist ein Keyword-Argument, das wir brauchen, um das Argument inc in den Namespace des lambda einzuschleusen. Keyword-Argumente lernen wir bald kennen.

map, filter, reduce

Python kennt einige Funktionen, die Funktionen auf Sequenzen anwenden:

  • map(fun, seq) – entspricht [fun(i) for i in seq]
  • filter(fun, seq) – enspricht [i for i in seq if fun(i)]
  • reduce(fun, seq)

reduce kann nicht ohne weiteres durch eine list comprehension ersetzt werden – ihr Pseudocode ist:

Eingabe: Eine Funktion fun, die zwei Argumente nimmt, eine Sequenz seq.

l := list(seq) Solange l länger als eins ist:

l[:2] = fun(l[0], l[1])
Gib l[0] zurück

Hier ist beispielsweise eine Implementation von string.join, die reduce verwendet:

def strjoin(sep, seq):
  return reduce(lambda a, b: a+sep+b, seq)

Eine berechtigte Frage ist: Wenn map und filter so einfach durch list comprehensions zu ersetzen sind – wer hat sie dann zu eingebauten Funktionen gemacht?

Zur Antwort muss man die berühmten “historischen Gründe” anführen – map und Freunde kamen relativ früh zu Python, list comprehensions erst viel später, und Guido van Rossum überlegt schon seit einiger Zeit, wie er sie wieder loswird.

Dennoch sollte man sich mit ihnen auseinandersetzen, unter anderem, weil sie im λ-Kalkül einfach auszudrücken sind und deshalb fester Bestandteil funktionaler Programmiertechniken sind.

Darüber hinaus eignen sie sich gut, um mit dem Konzept, dass Funktionen first class sind und man sie an andere Funktionen übergibt, damit diese sie aufrufen, warm zu werden. Häufig nennt man die Funktionen, die da übergeben werden, etwas respektlos callback. Im dritten Jahrtausend sollte niemand mehr von sowas überrascht sein, und tatsächlich verwenden alle modernen Programmiersprachen entsprechende Methoden in aller Breite.

>>> filter(lambda a:len(a)>0, ["bla",
... "", "romp", "bar", ""])
['bla', 'romp', 'bar']
>>> l = range(1,4);l
[1, 2, 3]
>>> reduce(lambda a,b: a*b, l)
6

Anmerkungen:

  • Das Funktionsargument zu filter prüft letztlich auf “Nicht-leer”, was für Strings das gleiche wie “wahr” ist. Deshalb ginge auch einfach lambda a:a. Noch schneller geht es mit dem Modul operator. In diesem Modul sind die Python-Operatoren als Funktionen, der Operator, den wir hier brauchen, heißt truth. Schneller ist das, weil operator.truth in C geschrieben ist, während unser lambda a:a immer noch in Python ist. Der Aufruf einer Python-Funktion dauert erheblich länger als der einer C-Funktion.
  • range kann nicht nur ein Argument nehmen, sondern auch zwei oder drei. Mit zwei Argumenten wird der erste als Anfang genommen, mit drei das dritte als Schrittweite. range(1,10,3) ist so [1, 4, 7].

Wir können damit unser Grammatik-Lesen etwas robuster machen:

  def _readRules(self, sourceName):
    self.rules = {}
    for rawRule in filter(operator.truth,
        map(str.strip,
          open(sourceName).readlines())):
      self._addRule(Rule(rawRule))

Übungen zu diesem Abschnitt

Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen

(1)

Wenn ihr eine Liste negativer Zahlen zwischen -n+ 1 und 0 haben möchtet – wie bekommt ihr sie mit Hilfe von map?

(2)

K.R. Acher möchte gerne eine Liste von (Wert, Schlüssel)-Paaren aus einem Dictionary haben und beschließt, per map die Paare, die er von der items-Methode zurückbekommt, umzudrehen. Er schreibt:

>>> d = {'a':1, 'b':2}
>>> def swapPair(pair):
...     return (pair[1], pair[0])
...
>>> map(swapPair(), d.items())
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: swapPair() takes exactly 1 argument (0 given)

Was hat Acher falsch gemacht?

(3)

Macht euch klar, was die Regeln sind, nach denen das dreiargumentige range seine Werte erzeugt. Schreibt Pseudocode, der das Verhalten simuliert. Probiert dazu erst ein paar interessante Grenzfälle am Rechner aus und verifiziert dann, dass sich euer Pseudocode genauso verhält.

(4)

Wie sieht das mit den Grenzfällen bei unserer strjoin-Funktion aus? Was gibt es für Grenzfälle, und wie schlägt sich die Funktion?

(5)

Implementiert die Fakultätsfunktion mit reduce.

(6)

Schreibt eine Factory Function makeBegCharChecker(char) -> Function für Funktionen, die jeweils Wahr zurückgeben, wenn ein übergebener String mit einem der Factory Function übergebenen Zeichen anfängt. Man soll also so etwas machen können:

>>> checkForA = makeBegCharChecker("A")
>>> checkFora = makeBegCharChecker("a")
>>> checkForA("Aachen")
True
>>> checkForA("Berlin")
False
>>> checkFora("Aachen")
False
>>> checkFora("aber")
True


Markus Demleitner

Copyright Notice