25.2 Das Modul functools 

Das Modul functools der Standardbibliothek enthält Funktionen und Decorators, mit deren Hilfe sich aufrufbare Objekte, beispielsweise Funktionen oder Methoden, auf einer abstrakten Ebene modifizieren lassen. In diesem Kapitel werden die Vereinfachung von Schnittstellen, das Hinzufügen eines Caches und das Vervollständigen der Ordnungsrelation besprochen.
[»] Hinweis
Dieser Abschnitt richtet sich an fortgeschrittene Leser und setzt teilweise Wissen aus anderen Kapiteln voraus, insbesondere aus Kapitel 21, »Objektorientierung«.
25.2.1 Funktionsschnittstellen vereinfachen 

Im Modul functools ist die Funktion partial enthalten, mit der sich Funktionsschnittstellen vereinfachen lassen. Betrachten Sie dazu die folgende Funktion:
def f(a, b, c, d):
print("{} {} {} {}".format(a,b,c,d))
Die Funktion f erwartet viele Parameter, vier an der Zahl. Stellen Sie sich nun vor, wir müssten die Funktion f sehr häufig im Programm aufrufen und übergäben dabei für die Parameter b, c und d immer die gleichen Werte. Mithilfe der Funktion partial lässt sich die Schnittstelle von f so verändern, dass nur der eigentlich interessante Parameter a übergeben werden muss.
partial(func, [*args], {**kwargs})
Die Funktion partial bekommt ein Funktionsobjekt übergeben, dessen Schnittstelle vereinfacht werden soll. Zusätzlich werden die zu fixierenden Positions- und Schlüsselwortparameter übergeben.
Die Funktion partial gibt ein neues Funktionsobjekt zurück, dessen Schnittstelle der von func entspricht, die jedoch um die in args (Arguments) und kwargs (Keyword Arguments) angegebenen Parameter erleichtert wurde. Bei einem Aufruf des zurückgegebenen Funktionsobjekts werden diese fixierten Parameter automatisch ergänzt.
Dies demonstriert das folgende Beispiel anhand der oben definierten Funktion f:
>>> def f(a, b, c, d):
... print("{} {} {} {}".format(a,b,c,d))
...
>>> import functools
>>> f_neu = functools.partial(f, b="du", c="schöne", d="Welt")
>>> f_neu("Hallo")
Hallo du schöne Welt
>>> f_neu("Tschüss")
Tschüss du schöne Welt
Zunächst wird die Funktion f definiert, die vier Parameter akzeptiert und diese hintereinander auf dem Bildschirm ausgibt. Da die letzten drei Parameter dieser Schnittstelle in unserem Programm immer gleich sind, möchten wir sie nicht immer wiederholen und vereinfachen die Schnittstelle mittels partial.
Dazu rufen wir die Funktion partial auf und übergeben das Funktionsobjekt von f als ersten Parameter. Danach folgen die drei feststehenden Werte für die Parameter b, c und d in Form von Schlüsselwortparametern. Die Funktion partial gibt ein Funktionsobjekt zurück, das der Funktion f mit vereinfachter Schnittstelle entspricht. Dieses Funktionsobjekt kann, wie im Beispiel zu sehen, mit einem einzigen Parameter aufgerufen werden. Der Funktion f wird dieser Parameter gemeinsam mit den drei fixierten Parametern übergeben.
Abgesehen von Schlüsselwortparametern können Sie der Funktion partial auch Positionsparameter übergeben, die dann ebenfalls als solche an die zu vereinfachende Funktion weitergegeben werden. Beachten Sie dabei, dass die feststehenden Parameter dann am Anfang der Funktionsschnittstelle stehen müssen. Dazu folgendes Beispiel:
>>> def f(a, b, c, d):
... print("{} {} {} {}".format(a,b,c,d))
...
>>> f_neu = functools.partial(f, "Hallo", "du", "schöne")
>>> f_neu("Welt")
Hallo du schöne Welt
>>> f_neu("Frau")
Hallo du schöne Frau
Die ersten drei Parameter der Funktion f sind immer gleich und sollen mithilfe der Funktion partial als Positionsparameter festgelegt werden. Das resultierende Funktionsobjekt f_neu kann mit einem Parameter aufgerufen werden, der beim daraus resultierenden Funktionsaufruf von f neben den drei festen Parametern als vierter übergeben wird.
25.2.2 Methodenschnittstellen vereinfachen 

Analog zur soeben besprochenen Funktion partial zur Vereinfachung von Funktionsschnittstellen existiert die Funktion partialmethod zur Vereinfachung von Methodenschnittstellen. Mithilfe von partialmethod lassen sich Varianten einer Methode erzeugen, bei denen bestimmte Parameter vorbelegt sind. Dazu folgendes Beispiel:
>>> import functools
>>> class Zitat:
... def __init__(self):
... self.quelle = "Unbekannt"
... def zitat(self, text):
... print("{}: '{}'".format(self.quelle, text))
... def setze_quelle(self, quelle):
... self.quelle = quelle
... setze_donald = functools.partialmethod(setze_quelle, "Donald Duck")
... setze_goofy = functools.partialmethod(setze_quelle, "Goofy")
...
>>> zitat = Zitat()
>>> zitat.setze_donald()
>>> zitat.zitat("Quack")
Donald Duck: 'Quack'
Im Beispiel wurden zwei Vereinfachungen der Methode setze_quelle zur Klasse Zitat hinzugefügt, die jeweils einen bestimmten Autor festlegen. Die Funktion partialmethod verfügt über dieselbe Schnittstelle wie partial, und das Festlegen von Parametern funktioniert nach dem gleichen Prinzip.
25.2.3 Caches 

Mithilfe des Decorators lru_cache, der im Modul functools enthalten ist, lässt sich eine Funktion mit einem Cache versehen. Ein Cache ist ein Speicher, der vergangene Funktionsaufrufe sichert. Wenn eine Parameterbelegung beim Funktionsaufruf bereits vorgekommen ist, kann das Ergebnis aus dem Cache gelesen werden, und die Funktion muss nicht noch einmal ausgeführt werden. Dieses Prinzip kann besonders bei rechenintensiven und häufig aufgerufenen Funktionen einen großen Laufzeitvorteil bringen.
[»] Hinweis
Wenn ein Funktionsergebnis aus dem Cache gelesen wird, wird die Funktion nicht ausgeführt. Das Cachen ergibt also nur Sinn, wenn die Funktion frei von Seiteneffekten und deterministisch ist, also das Ergebnis bei der gleichen Parameterbelegung stets dasselbe ist.
lru_cache([maxsize, typed])
Der Decorator lru_cache versieht eine Funktion mit einem LRU Cache mit maxsize Einträgen. Bei einem LRU Cache (für Least Recently Used) verdrängt ein neuer Eintrag stets den am längsten nicht mehr aufgetretenen Eintrag, sofern der Cache vollständig gefüllt ist. Wenn für maxsize der Wert None übergeben wird, hat der Cache keine Maximalgröße und kann unbegrenzt wachsen. Der mit False vorbelegte Parameter typed gibt an, ob gleichwertige Instanzen verschiedener Datentypen, zum Beispiel 2 und 2.0, als gleich (False) oder als ungleich (True) angesehen werden sollen.
Im folgenden Beispiel wird die Funktion fak zur Berechnung der Fakultät einer ganzen Zahl definiert und mit einem Cache versehen:
>>> import functools
>>> @functools.lru_cache(20)
... def fak(n):
... res = 1
... for i in range(2, n+1):
... res *= i
... return res
...
>>> [fak(x) for x in [7, 5, 12, 3, 5, 7, 3]]
[5040, 120, 479001600, 6, 120, 5040, 6]
Mithilfe der Methode cache_info, die der Decorator lru_cache dem dekorierten Funktionsobjekt hinzufügt, erhalten Sie Informationen über den aktuellen Status des Caches:
>>> fak.cache_info()
CacheInfo(hits=3, misses=4, maxsize=20, currsize=4)
Das Ergebnis ist ein benanntes Tupel mit den folgenden Einträgen:
Eintrag | Beschreibung |
---|---|
hits | die Anzahl der Funktionsaufrufe, deren Ergebnisse aus dem Cache gelesen wurden |
misses | die Anzahl der Funktionsaufrufe, deren Ergebnisse nicht aus dem Cache gelesen wurden |
maxsize | die maximale Größe des Caches |
currsize | die aktuelle Größe des Caches |
Tabelle 25.1 Einträge im CacheInfo-Tupel
Zusätzlich zu cache_info verfügt ein mit lru_cache dekoriertes Funktionsobjekt über die parameterlose Methode cache_clear, die den Cache leert.
[»] Hinweis
Intern wird der Cache in Form eines Dictionarys realisiert, bei dem die Parameterbelegung eines Funktionsaufrufs als Schlüssel verwendet wird. Aus diesem Grund dürfen nur Instanzen von hashbaren Datentypen an ein Funktionsobjekt übergeben werden, das den hier vorgestellten LRU Cache verwendet.
25.2.4 Ordnungsrelationen vervollständigen 

Eine Klasse, auf der eine Ordnungsrelation definiert sein soll, für die also die Vergleichsoperatoren <, <=, >, >= funktionieren sollen, muss jede der entsprechenden magischen Methoden __lt__, __le__, __gt__ und __ge__ implementieren, obwohl eine dieser Methoden bereits ausreichen würde, um die Ordnungsrelation zu beschreiben.
Der Decorator total_ordering, der im Modul functools enthalten ist, erweitert eine Klasse, die nur eine der oben genannten magischen Methoden und zusätzlich die Methode __eq__ bereitstellt, um die jeweils anderen Vergleichsmethoden. Das folgende Beispiel demonstriert die Verwendung des Decorators:
>>> import functools
>>> @functools.total_ordering
... class MeinString(str):
... def __eq__(self, other):
... return max(self) == max(other)
...
... def __lt__(self, other):
... return max(self) < max(other)
...
>>> MeinString("Hallo") > MeinString("Welt")
False
>>> MeinString("Hallo") <= MeinString("Welt")
True
Die Klasse MeinString erbt von dem eingebauten Datentyp str. Instanzen von MeinString sollen anhand des größten enthaltenen Buchstabens miteinander verglichen werden. Dazu sind die Methoden __eq__ für den Gleichheitsoperator und __lt__ für den Kleiner-Operator implementiert. Da die Klasse mit total_ordering dekoriert wurde, können auch die nicht explizit implementierten Vergleichsoperatoren verwendet werden.
25.2.5 Überladen von Funktionen 

Es gibt Operationen, die für Instanzen verschiedener Datentypen definiert sind, aber je nach Datentyp unterschiedlich implementiert werden müssen. Ein Beispiel für solch eine Operation ist die eingebaute Funktion print, die anhand der übergebenen Datentypen eine Ausgabevariante auswählt.
Das Modul functools enthält den Decorator singledispatch, der das Überladen von Funktionen ermöglicht. Beim Überladen werden Implementierungsvarianten einer Funktion unter dem gleichen Namen hinzugefügt. Bei einem Aufruf der Funktion wählt der Interpreter anhand des Datentyps der übergebenen Parameter aus, welche konkrete Variante ausgeführt wird. Im Falle von singledispatch wird die auszuführende Variante anhand des Datentyps des ersten übergebenen Parameters ausgewählt, daher der Name.
Im folgenden Beispiel wird die Funktion mult definiert, die sich nicht um die ihr übergebenen Datentypen kümmert und daher ein unterschiedliches Verhalten für Zahlen und Strings aufweist:
>>> def mult(x):
... return x*2
...
>>> mult(5)
10
>>> mult("5")
'55'
Mithilfe des Decorators singledispatch lässt sich die Funktion für Strings überladen, sodass in diesem Fall eine Multiplikation auf dem im String enthaltenen Zahlenwert durchgeführt wird:
>>> import functools
>>> @functools.singledispatch
... def mult(x):
... return x*2
...
>>> @mult.register(str)
... def _(x):
... return str(int(x)*2)
...
Die Ausgangsfunktion mult wird wie im vorangegangenen Beispiel definiert und zusätzlich mit dem Decorator singledispatch versehen. Dieser Decorator erweitert sie um die Methode register, mithilfe derer sie sich überladen lässt.
Im zweiten Teil des Beispiels wird eine Variante der Methode mult für Strings implementiert. Diese Variante, die den temporären Namen »_« trägt, wird über den Decorator mult.register als Variante von mult registriert. Je nach Parameter wird jetzt eine der beiden Varianten von mult ausgeführt:
>>> mult(5)
10
>>> mult("5")
'10'
Auf diese Weise lässt sich eine Funktion beliebig oft überladen. Wenn eine Variante für mehrere Datentypen verfügbar sein soll, können die register-Decorator verkettet werden:
>>> @mult.register(float)
... @mult.register(str)
... def _(x):
... return str(int(x)*2)
...
>>> mult(5.0)
'10'
>>> mult("5")
'10'
[»] Hinweis
Diese Art der Funktionsüberladung ist kein grundlegendes Konzept von Python, sondern wurde eingeführt, damit grundlegende Funktionen der Standardbibliothek, darunter print oder len, bequem in Python implementiert werden können. Die Funktionsüberladung in Python ist daher mit großen Einschränkungen verbunden:
- Es können nicht beliebige Funktionen überladen werden, sondern nur solche, die mit singledispatch dekoriert wurden.
- Eine überladbare Funktion darf nur über einen einzigen nicht-optionalen Parameter verfügen.
- Andere Sprachen, beispielsweise C++, bieten umfassende Freiheiten beim Überladen von Funktionen. Davon müssen Sie diesen Ansatz unterscheiden.