garbledina.util.dispatch
Als dynamisch typisierte Sprache, in der man keine Typen deklarieren muss (oder kann), hat Python keine Methoden- und Funktionsüberladung. Da es aber Programmierprobleme, die sich damit vielleicht eleganter lösen lassen als mit Introspektion (und/oder Duck-Typing), und da ein Typencheck in einer else-if-Kette alles andere als elegant ist, gibt es verschiedene Versuche, Typendeklarationen (optional) in die Sprache einzubauen. Python 3000 wird z.B. sog. Annotations ermöglichen, mit denen man die Argumente und den Rückgabewert einer Funktion mit beliebigen Objekten "dekorieren" kann. Aussehen wird das so:
def f(a : int, b : float) -> str: ...
Die Annotations sind dann als Dictionary unter f.__annotations__ verfügbar. Es ist dann die Aufgabe eines Decorators oder ähnlichem, daraus etwas sinnvolles zu machen. Eine Möglichkeit wäre z.B. das Überladen von Funktionen und Methoden zu ermöglichen.
Da mir die existierenden Implementationen von Multimethoden nicht gefallen haben (oder ich sie bevor ich mit dem Programmieren angefangen habe noch nicht kannte), habe ich mir zwei eigene Module dazu geschrieben.
Das erste, garbledina.util.annotate, enthält nur einen Decorator (annotate), der das obige Beispiel in Python 2.4 aufwärts folgendermaßen aussehen lässt:
@annotate(a=int, b=float, **{"return" : str})
def f(a, b):
...
Für return könnte man sich noch etwas besseres einfallen lassen, aber dazu hatte ich bisher noch keinen Grund, weil ich diese Funktionalität nicht benötige.
Nun aber zum Titel des Eintrages und dem, worüber ich eigentlich schreiben wollte: In garbledina.util.dispatch existiert ein Decorator, multifunction, der es möglich macht, zusammen mit annotate mehrere Implementationen einer Funktion unter einem Namen zur Verfügung zu stellen. Ich habe mir dabei Mühe gegeben, die Verwendung möglichst bequem zu machen, damit der resultierende Code möglichst natürlich und lesbar aussieht:
@multimethod @annotate(a=int) def f(a): print "a is an int:", a @multimethod @annotate(a=float) def f(a): print "a is an float:", a
Zu beachten ist, dass in Python Decorator in umgekehrter Reihenfolge ausgeführt werden. Dies mag zwar zunächst ungewöhnlich erscheinen, macht aber gerade das Beispiel lesbarer und ist auch natürlich, wenn man bedenkt, dass die Funktionsdefinition zuerst ausgeführt wird. Das Beispiel definiert also zweimal die Funktion f, einmal für Ganzzahlen und einmal für Gleitpunktzahlen. Dabei werden auch Subklassen erkannt, d.h. eine Implementation für object würde alle verbliebenen Fälle fangen. Dies funktioniert auch für Funktionen mit verschieden vielen Argumenten und Default-Werten, wobei hier die kürzeste Übereinstimmung genommen wird. Funktionen mit variablen Argumentlisten (*args) sind nicht unterstützt und beliebige Keywords (**kwds) werden einfach ignoriert. Ich weiß noch nicht, ob es sich lohnt, diese Fälle zu implementieren.
Das die beiden Funktionen zusammen gehören, wird global am Namen erkannt, d.h. dass die verschiedenen Implementationen über verschiedene Module verteilt sein können (obwohl ich mir nicht sicher bin, ob das "pythonic" ist). Es ist auch möglich eine Implementation explitzit an einer Multifunktion zu registrieren.
Worauf ich in der aktuellen Version besonders stolz bin, ist, dass in der aktuellen Version auch Methoden richtig funktionieren. Methoden und Funktionen sind ja in Python sehr eng verwandt (dazu vielleicht ein späterer Artikel) und das habe ich versucht auch in den Multimethods nachzubilden. Hinzu kommt, dass auch das annotieren des ersten Parameters (self) richtig funktioniert, was etwas schwierig war, da das Klassenobjekt der Klasse, in der die Methoe definiert wird, zur Definitionszeit der Methode noch nicht existiert und deshalb nicht an annotate übergeben werden kann. Eigentlich will man dies auch gar nicht, denn der Typ von self ist ja schon durch den Kontext der Methodendefinition klar. Genau diesen Kontext benutze ich mittels der Verwendung von schwarzer Bytecode-Magie und ... na? ... Metaklassen. :-)
Dies kann dann also folgendermaßen aussehen:
class Test(object):
@multimethod
@annotate(a = int)
def test(self, a):
print "A"
@multimethod
@annotate(a = float)
def test(self, a):
print "B"
class Test2(Test):
@multimethod
@annotate(a = int)
def test(self, a):
print "C"
Verwendung:
t = Test() t.test(1) # -> A t.test(1.0) # -> B t2 = Test2() t2.test(1) # -> C t2.test(1.0) # -> B
Gerade den letzten Aufruf richtig hinzubekommen, hat mir etwas Kopfzerbrechen bereitet und ich muss noch versuchen, den Code dazu noch klarer zu strukturieren, weil ich sonst bald nicht mehr verstehe, warum das funktioniert. Jedenfalls bin ich stolz genug darauf, dass ich überlege dispatch.py als separates Paket zur Verfügung zu stellen und zur Inklusion in Python anzubieten. (Ein bisschen Motivation würde helfen. :-)