Dienstag, 25. März 2014

Klammeraffen

Wenn man ein wenig mit Python rumspielt, stolpert man früher oder später über all die lustigen Dinge, die man mit Funktionen anstellen kann. Über die Vorzüge von funktionaler Programmierung brauche ich mich nicht auslassen, das haben schon andere getan. Jedenfalls hat sich die Python-Gemeinde irgendwann entschieden (ich bin zu faul, den genauen PEP rauszusuchen), die aus anderen Sprachen bekannten @-Dekoratoren einzuführen. (Vielleicht haben sie die auch erfunden, die Historie spielt für diese Anwendung keine Rolle.) Jedenfalls sind diese Dekoratoren in Python sehr einfach:

def decorated(func):
    def wrapper(*args, **kvargs):
        return func(*args, **kvargs)
    return wrapper


@decorated
def my_func():
    """my funky function"""
    pass

Will man dem Dekorator noch Argumente mitgeben, wird es ein wenig umständlicher:

def decorated(msg):
    def decorator(func):
        def wrapper(*args, **kvargs):
            print msg
            return func(*args, **kvargs)
        return wrapper
    return decorator


@decorated("nice function")
def my_func():
    """my funky function"""
    pass

Ich gebe zu, beim ersten Mal lesen erschließt sich das nicht unbedingt gleich: Der Code des Dekorators wird bei der Deklaration der dekorierten Funktion ausgeführt und liefert als Ergebnis die Funktion, die dann später bei Bedarf ausgeführt wird. Mit anderen Worten: Die dekorierte Funktion wird unsichtbar, an ihre Stelle tritt das Ergebnis des Dekorator-Aufrufs. Ach, ich geb's auf, immer wenn ich versuche, das zu erklären, wird es nur verworrener -- es sollte ausreichen zu wissen, dass es dutzende gute und einfache Erklärungen für die Funktionsweise gibt. Dummerweise ist das so einfach, dass man immer geneigt ist zu glauben, da müsse noch irgendwas kompliziertes, magisches im Hintergrund geben. Gibt es nicht.

Oder vielleicht doch: Die dekorierte Funktion heißt jetzt für alle Welt wrapper, und ihr fehlen solche Dinge wie der Docstring, den wir mühsam auf my funct function gesetzt haben. Das kann man jetzt mühsam händisch nachrüsten, oder man verwendet wraps aus den functools. Was wohl die bessere Idee ist?

def haendisch(func)
    def wrapper(*args, **kvargs):
        return func(*args, **kvargs)
    doc = getattr(func, '__doc__', None)
    if doc:
        setattr(wrapper, '__doc__', doc)
    return wrapper


import functools
def bessere_idee(func):
    @functools.wraps(func)
    def wrapper(*args, **kvargs):
        return func(*args, **kvargs)
    return wrapper

Die Funktion bessere_idee sieht schon besser aus, wie ich finde. wraps kümmert sich rührend um Funktionsnamen und Docstring -- einfach immer verwenden, wenn man irgendwelche Funktionen wrapped. Und nicht vergessen: Immer wenn man eine wirklich coole Idee für einen Dekorator hat, der irgendwas wildes mit der Funktion, der Klasse oder dem Objekt anstellen kann -- je weniger wild und cool, desto besser.

Keine Kommentare: