Mit Python-Dekoratoren lässt sich die Kernfunktionalität einer Anwendung von der erweiterten Funktionalität trennen. Ein Codebeispiel, das die Berechnung von Fibonacci-Zahlen mit einem Wertecache beschleunigt, demonstriert die Eleganz dieses aspektorientierten Ansatzes.
Ein Programmierer schreibt eine Funktion, die Thread-sicher sein muss und außerdem die programmweite Logdatei nutzen soll. Alles, was er dazu tun muss, ist, der Funktion die Auszeichnungen »@synchronized« und »@logging« voranzustellen. Mit Python-Dekoratoren ist eine solch elegante Annotation von Funktionen tatsächlich möglich.
Selbstverständlich muss der Entwickler den für die hinzugefügte Funktionalität erforderlichen Programmcode trotzdem schreiben. Sein nachträgliches Anbinden an bestehende Funktionen eröffnet dennoch interessante Möglichkeiten. Viele Forderungen des aspektorientierten Programmierparadigmas lassen sich mit ihnen umsetzen (Abbildung 1).

Abbildung 1: Der aspektorientierten Programmierung geht es darum, zur Kernfunktionalität orthogonal stehende Seitenaspekte wie Datenvalidierungen getrennt zu entwickeln und erst spät in die Anwendung einzuflechten.
Anwendungsfälle
Dekoratoren sind zum Beispiel nützlich, wenn es darum geht, das implizite Verhalten eines Programms sichtbar zu machen, zum Beispiel Laufzeitverhalten, Programmfluss oder Aufrufhäufigkeit von Funktionen. Funktionen lassen sich außerdem mit Vor- und Nachbedingungen oder beispielsweise mit Cache-Mechanismen ausstatten.
Dieser Artikel zeigt am Beispiel der Fibonacci-Zahlen, wie Dekoratoren den Entwickler zunächst bei der Analyse von Performanceproblemen helfen. Aus den dabei gewonnenen Erkenntnissen ergibt sich, dass ein Caching die Zahl der benötigten Durchläufe stark verringert. Die Performance steigt dabei um weit mehr als den Faktor 1000.
Entflochten
Bei der Software-Entwicklung ist es sinnvoll, Kernfunktionalität und erweiterte Funktionalität wie Sicherheits-Check, Datenvalidierungen oder Protokollierung möglichst sauber zu trennen (Abbildung 1). Die Grundidee der aspektorientierten Programmierung ist es, diese Core-Level-Concerns getrennt von den orthogonal zu ihnen stehenden System-Level-Concerns zu entwickeln und erst im letzten Schritt zu verflechten.
Python-Aspekte
Unter Python lässt sich dies effizient mit Dekoratoren umsetzen. Aspekte, die sich durch die ganze Kernfunktionalität ziehen, extrahieren die Dekoratoren und definieren sie modulübergreifend. So ergeben sich folgende Vorteile:
- Die Kernfunktionalität lässt sich von der erweiterten
Funktionalität getrennt entwickeln. - Die erweiterte Funktionalität lässt sich dynamisch
an- und ausschalten. - Der Code ist expliziter und daher besser verständlich,
wartbar und testbar. - Die leichte Anbindung der erweiterten Funktionalität
erleichtert ein Wiederverwenden.
Der Name Dekorator ist eigentlich unglücklich gewählt, da Dekoratoren sich nicht mit dem klassischen Dekorator-Design-Pattern aus der objektbasierten Programmierung vergleichen lassen (siehe Kasten “Dekoratoren und Dekorator-Pattern”).
Wie alles anfing
Seit der Version 2.2 kennt Python Klassenmethoden und statische Methode. Sie entstanden durch nachträgliche Deklaration aus gewöhnlichen Instanzmethoden: »bar= classmethod(bar)« oder »foo= staticmethod(foo)« . Zwar ist die Syntax bis heute gültig, aber weit weniger elegant als die seit Python 2.4 in Anlehnung an Javas Annotations nun gebräuchliche Dekorator-Schreibweisen »@staticmethod« und »@classmethod«. Mit diesen beiden Auszeichnungen ist der Leistungsumfang von Dekoratoren in Python aber noch lange nicht erschöpft.
|
Dekoratoren und |
|---|
|
Das Dekorator-Pattern entstammt dem Bereich der objektbasierten Strukturmuster. Beim objektorientierten Programmieren kommt es zum Einsatz, um ein bestehendes Objekt um Zusatzfunktionalität zu erweitern. Dabei schließt das dekorierende Objekt das dekorierte Objekt ein. Da die Schnittstelle des dekorierenden Objekts der des dekorierten Objekts entspricht, schränkt dieses Vorgehen die Wiederverwendbarkeit des Code nicht ein. Hier zeigt sich der Unterschied zum Dekorator deutlich: Der Dekorator nimmt in der Regel eine Funktion an, bindet sie im Dekorator-Kontext und delegiert den Aufruf an die dekorierte Funktion. Dagegen arbeitet das Dekorator-Pattern auf der Ebene von Objekten, die es bindet und an die es die Methodenaufrufe delegiert. |
Unter der Haube
Ein Dekorator in Python ist ein aufrufbares Objekt, das die zu dekorierende Funktion als Argument annimmt und mit zusätzlicher Funktionalität ausstattet. Diese lässt sich dann unter dem Namen der dekorierten Funktion ansprechen. Im Augenblick seiner Instanzierung muss der Dekorator seinen Zustand speichern. Hierfür bieten sich in Python zwei Techniken an, Closures und aufrufbare Objekte (Callable Objects).
Closures definiert der Entwickler als Funktionen in Funktionen. Die innere Funktion speichert dabei im Augenblick der Ausführung der äußeren ihren Aufrufkontext. Closures verhelfen also Funktionen zu einem Gedächtnis. Objekte hingegen besitzen in Form von Attributen das Gedächtnis von Haus aus. Aber erst nach dem Implementieren des »__call__«-Operators lässt sich das Objekt aufrufen. In C++ entstehen aufrufbare Objekte, die so genannten Funktoren, durch Überladen des Klammer-Operators.
Listing 1 zeigt einfache Dekoratoren als Closure und Objekt umgesetzt. Beide Fassungen geben nur eine kurze Nachricht auf der Konsole aus. Da beide Techniken gleich mächtig sind, hängt es ausschließlich von der Vorliebe des Programmierers ab, ob er Dekoratoren funktional mit Closures oder objektorientiert als aufrufbare Objekte implementiert.
Vereinfachend betrachtet bedeutet das Dekorieren einer Funktion das Erweitern der Funktionalität unter dem altem Namen. Der Dekorator »decorator()« nimmt die Funktion »func()« als Argument entgegen, bindet sie und gibt eine neue Funktion zurück, die nun »func()« heißt. Mächtige neue Funktionen entstehen aus der Verkettung mehrerer Dekoratorenaufrufe. Dies setzt natürlich voraus, dass der Rückgabewert eines Dekorators sich als Argument für den nächsten Dekorators verwenden lässt.
Wichtig ist es dafür, zwischen die Signatur bewahrenden und die Signatur verändernden Dekoratoren zu unterschieden. Dekoratoren, die die Signatur bewahren, lassen sich einfacher verketten.
|
Listing 1: Einfache |
|---|
01 def decorator(func): 02 def closure(*__args,**__kw): 03 print "Hello I'm a closure." 04 return func(*__args,**__kw) 05 return closure 06 07 class Decorator(object): 08 def __init__(self,func): 09 self.__func = func 10 def __call__(self,*__args,**__kw): 11 print "Hello I'm a callable function." 12 return self.__func(*__args,**__kw) |

Abbildung 2: Analysetool: Der Dekorator »Called« zählt fast zehn Millionen Aufrufe der rekursiven Fibonacci-Funktion.
Implementierung leicht
Zur Erinnerung: Die Fibonacci-Folge besteht aus einer unendliche Folge von Fibonacci-Zahlen, die sich durch Addition der beiden vorherigen Zahl ergeben. Dies ist wirklich nicht schwer zu implementieren:
def fibonacci(n): "Return the nth fibonacci number." if n in (0,1): return n return fibonacci(n-1) + fibonacci(n-2)
Eine Schleife um diese Funktion soll nun jede fünfte Fibonacci-Zahl bis 100 errechnen. Übergibt der Anwender diese dem Python-Interpreter, dann bleibt ihm in der Praxis allerdings nichts anderes übrig, als den Programmablauf mit [Strg]+[C] zu unterbrechen, ihre Laufzeit ist einfach bei Weitem zu lange. Da eine Addition nicht teuer ist, muss es an der Rekursionshäufigkeit liegen. Dass dies tatsächlich so ist, lässt sich durch einen Dekorator beweisen, der die Anzahl der Rekursionen zu jedem Funktionsaufruf mitzählt (Listing 2).
|
Listing 2: Funktionsaufrufe |
|---|
01 class Called(object): 02 "Decorator that keeps track of the number of times a function is called." 03 def __init__(self,f): 04 self.__f = f 05 Called.__numCalls=0 06 def __call__(self,n): 07 Called.__numCalls += 1 08 return self.__f(n) 09 @classmethod 10 def count(cls): 11 return cls.__numCalls |
Der Dekorator »Called()« erhält im Initializer »__init__« die Funktion »f«, die er überwacht und im »__call__«-Operator ausführt. Durch diese Methode verhält sich der Dekorator wie eine aufrufbare Funktion. In der Klassenvariablen »Called.__numCalls« protokolliert er mit, wie oft er aufgerufen wurde. Deren Wert lässt sich dann über die Klassenmethode »count()« auslesen.
Ein einfacher Test für die Fibonacci-Zahl 35 bestätigt nun den Verdacht: Ein Aufruf der »fibonacci()«-Funkti-on für den Wert 35 führt zu nahezu zehn Millionen Funktionsaufrufen (Abbildung 2).
In die Tiefe
Die Rekursion ruft »Fibonacci()« mit Werten zwischen 0 und 35 auf. Nun ist es interessant, zu erfahren, welche Werte für den größten Teil der Rechenzeit verantwortlich sind. Dazu setzt der verbesserte Dekorator »CalledWithSignatur()« ein Dictionary ein, in dem er die Aufrufhäufigkeit abhängig vom aufgerufenen Wert zählt (Listing 3). Der Schlüssel für den Dictionary-Eintrag besteht aus dem Funktionsnamen und der Signatur der Funktion.
Der Aufruf für die Fibonacci-Zahl von 35 ergibt die Verteilung aus Abbildung 3. Nun liegt das Laufzeitproblem der rekursiven Errechnung der Fibonacci-Zahlen auf der Hand: Je niedriger die Fibonacci-Zahlen sind, desto häufiger sind sie zu errechnen. Das Problem lässt sich darum sehr einfach lösen: Ein effizienter Algorithmus speichert einmal errechnete Werte zwischen.

Abbildung 3: Das signaturabhängige Zählen der Aufrufe ergibt: Die kleinen Fibonacci-Zahlen sind für den Löwenanteil der Laufzeit verantwortlich.
Genau dies tut der Dekorator »Memo« in Listing 4. Er baut ein Gedächtnis im Dictionary »__cache« auf. Nach dem Wert einer Fibonacci-Zahl befragt, sieht er zuerst im Cache nach. Falls die Fibonacci-Zahl dort nicht vorhanden ist, führt dies zu einer »KeyError«-Exception. Zeile 10 fängt diese auf, der Exception-Block (Zeilen 11 bis 13) berechnet anschließend die Fibonacci-Zahl, gibt sie zurück und speichert sie. Der Dekorator »Memo()« hält, was er verspricht: Jede fünfte Fibonacci-Zahl einschließlich der 100 ist nun – anders als mit Listing 2 möglich – schnell berechnet (Abbildung 4).
Einfache Rechnung, aber 1000 ist ein Problem
Das Verketten der Dekoratoren aus Listing 3 und Listing 4 beweist, dass jede Fibonacci-Zahl nur einmal zu berechnen war (Listing 5 und Abbildung 5). Leider ist es in dieser Version des Memo-Dekorators nicht möglich, die Fibonacci-Zahl von 1000 zu berechnen. Dies scheitert an der maximalen Rekursionstiefe. Zwar ließe sich diese plattformabhängig erhöhen, allerdings nicht beliebig weit. Doch mit einer erweiterten Version von Listing 4 lässt sich selbst die Fibonacci-Zahl von 20000 berechnen:
try: print "fibonacci(1000)", fibonacci(1000) except RuntimeError, e : print e
Durch schrittweises Aufrufen der Fibonacci-Zahlen in der Schrittgröße 200 baut der Dekorator sukzessive sein Gedächtnis auf. Dadurch errechnet er die Fibonacci-Zahl von 20000, eine Zahl mit 4180 Stellen, in Sekundenschnelle (Abbildung 6).
|
Listing 3: Aufrufe nach Signatur |
|---|
01 class CalledWithSignatur(object):
02 "Decorator that keeps track of the number of times a function is called,
03 depending on the signature"
04 __instance={}
05 def __init__(self,f):
06 self.__f = f
07 self.__funcName= f.__name__
08 CalledWithSignatur.__instance={}
09 def __call__(self,n):
10 funcNameArgs=self.__funcName+"("+str(n)+")"
11 CalledWithSignatur.__instance[funcNameArgs]=
12 CalledWithSignatur.__instance.setdefault(funcNameArgs,0) + 1
13 return self.__f(n)
14 @classmethod
15 def count(cls):
16 return CalledWithSignatur.__instance
|

Abbildung 5: Dekoratoren lassen sich verketten. Hier liegt der Zähl-Dekorator über dem Cache-Dekorator und beweist, dass das Programm jeden Fibonacci-Wert nur noch einmal berechnet.
Fallstricke
Der Aufruf einer Funktion durch einen Dekorator stellt stets eine Indirektion dar. Das Verketten von Dekoratoren verstärkt den Effekt noch. Neben der Indirektion wirkt sich die zusätzliche Funktionalität des Dekorators negativ auf das Laufzeitverhalten des Funktionsaufrufs aus.

Abbildung 6: Noch eins draufgesetzt: Der neue »try«-Block verschachtelt die Rekursion, sodass sich die Fibonacci-Zahl von 20000 in wenigen Sekunden errechnen lässt.
Sind Dekoratoren zu simpel implementiert, dann überschreiben sie die Meta-Information einer Funktion mit ihren eigenen Informationen. Auf diese Weise verliert die Funktion »fibonacci()« unglücklicherweise ihre ursprünglichen Funktionsattribute und nimmt dafür die des Dekorators Memo an. Bei als Closure implementierten Dekoratoren reicht es, den Metadekorator »simple_decorator« aus der Python Decorator Library [2] dem eigentlich Dekorator voranzustellen. Dann bleiben die Meta-Informationen aller Beteiligten erhalten.
Die Deko-Welle rollt
Mittlerweile existiert ein reicher Fundus fertiger Dekoratoren. Ein Blick auf die Python Decorator Library [2], Michele Simionatos Dekorator Modul [3] oder das Python Cookbook [4] lohnt sich. Dekoratoren haben sich in Python als das gängigste Mittel etabliert, um Metaprogramming, also das Erzeugen von Programmcode über Metacode, umzusetzen. Sie bilden eine Abstraktion über Funktionen, die es erlaubt, sie rein deklarativ zu erweitern. (pkr)
|
Listing 4: Werte |
|---|
01 class Memo(object):
02 """Decorator that caches the function invocation result for later
03 invocations"""
04 __cache={}
05 def __init__(self, func):
06 self.__func = func
07 def __call__(self, *args):
08 try:
09 return self.__cache[args]
10 except KeyError:
11 value = self.__func(*args)
12 Memo.__cache[args]= value
13 return value
|
|
Listing 5: Dekoratoren |
|---|
01 @Memo 02 @CalledWithSignatur 03 def fibonacci(n): 04 "Return the nth fibonacci number." 05 if n in (0,1): return n 06 return fibonacci(n-1) + fibonacci(n-2) |
|
Infos |
|---|
|
[1] New-Style-Klassen: [http://docs.python.org/3.0/library/abc.html] [2] Python Decorator Library: [http://wiki.python.org/moin/PythonDecoratorLibrary] [3] Dekorator Modul: [http://pypi.python.org/pypi/decorator] [4] Python Cookbook: [http://code.activestate.com/recipes/langs/python/] |
|
Der Autor |
|---|
|
Rainer Grimm arbeitet seit 1999 als Software-Entwickler bei der Science + Computing AG in Tübingen. Insbesondere hält er Schulungen für das hauseigene Produkt SC Venus. |






