Decorator – Grundlagen für Anfänger

Die Decorator-Funktion ist ein ziemlich fortgeschrittener Teil der Programmiersprache Python. Wie die meisten Dinge wird auch der Decorator sehr einfach, wenn man erst einmal verstanden hat, wie er funktioniert, und ihn ein paar Mal verwendet hat, aber für Anfänger kann er ein wenig entmutigend und schwer zu verstehen sein.

Die Definition

Ein Decorator ist eine Funktion, die eine andere Funktion als Argument nimmt und eine modifizierte Version von ihr zurückgibt, die ihre Funktionalität in irgendeiner Weise erweitert.

Wenn man bereits verstanden hat, was ein Dekorator ist, ist diese Definition vollkommen klar, aber wenn nicht – vielleicht nicht so sehr. Und entscheidend ist, dass die Definition allein einem nicht sagt, wann man einen Decorator verwenden sollte.

Also, zur Praxis. Wir werden mit einem hypothetischen Szenario beginnen und die Probleme beobachten, die sich entfalten können, wenn man keinen Decorator benutzt.

Die Praxis

Du wurdest gerade als Entwickler bei ‚Pro String Inc. Ltd‘ eingestellt – ein Unternehmen, das seinen Kunden fortgeschrittenen Code zur Stringmanipulation anbietet. An Deinem ersten Tag kommt Dein Chef zu Dir und bittet Dich, eine Funktion zu schreiben, die aus einer Zeichenkette ein Palindrom macht: eine Zeichenkette, die sich vorwärts wie rückwärts gleich liest.

Der Code könnte so aussehen:

def make_palindrome(string):
  """Makes a palindromic mirror of a string."""

    return string + string[::-1]

So weit, so gut. Eine Stunde später kommt der Chef zurück und bittet um weitere Funktionen: eine, um einen Abspann an das Ende einer beliebigen Zeichenkette anzuhängen, eine, um eine Zeichenkette in snake_case in eine in camelCase umzuwandeln, und eine, um Kommas in eine Zeichenkette einzufügen. Alles lebenswichtige Funktionen. Du machst Dich an die Arbeit.

Hier das Ergebnis:

def add_credits(string):
    """Adds the company's credits to the end of any string."""
    return f"{string} (string created by Pro String Inc.)"


def snake_to_camel(string):     """Converts a string in snake_case to camelCase."""     words = string.split("_")
    if len(words) > 1:        words = [words[0]] + [word.title() for word in words[1:]]
    return "".join(words)

def insert_commas(string, spacing=3):   """Inserts commas between every n characters."""   sections = [string[i: i + spacing] for i in range(0, len(string), spacing)]
  return ",".join(sections)

Schön. Du hast jetzt vier Funktionen geschrieben, die alle eine Zeichenkette als Argument nehmen und eine andere Zeichenkette ausgeben – und das ist erst Dein erster Tag.

Nach dem Mittagessen taucht jedoch ein Problem auf. Derselbe Chef sieht sich Deinen Code an und erinnert Dich daran, dass alle Funktionen von Pro String Inc. Ltd. in der Lage sein müssen, Ganzzahlen als Eingaben zu akzeptieren, und dass diese in Strings umgewandelt werden sollten. Er empfiehlt, am Anfang jeder Funktion eine Zeile einzufügen, die prüft, ob die Eingabe eine Ganzzahl ist, und sie gegebenenfalls umwandelt.

Das demoralisiert Dich – wenn man Deinem Chef glauben darf, musst Du jede Funktion durchgehen und so etwas an den Anfang stellen…

if isinstance(string, int):
    string = str(string)

Wirklich? Das ist (irgendwie) in Ordnung, wenn wir vier Funktionen haben, die wir ändern müssen, aber was wäre, wenn wir zehn hätten? Oder tausend? Und abgesehen von der Arbeit verstößt es gegen das heilige Gesetz „Wiederhole dich nicht“ („Don’t Repeat Yourself“), wenn alle Funktionen mit denselben zwei Zeilen beginnen. Gibt es nicht eine Möglichkeit, all diese Funktionen einfach zu ändern, ohne ihnen zusätzliche Zeilen hinzuzufügen?

Offensichtlich ja, sonst würde wir uns nicht mit dem Thema ‚Decorator‘ befassen. Um zu sehen, wie das geht, sollten wir einen Schritt zurückgehen und uns die Python-Funktionen ansehen.

Trotz ihrer speziellen Syntax ist eine Python-Funktion einfach ein Objekt, wie eine Zeichenkette oder eine Liste. Man kann ihre Attribute überprüfen, sie neuen Variablen zuweisen und – ganz wichtig – sie als Argument an eine andere Funktion übergeben. Man kann zum Beispiel eine Funktion erstellen, die eine andere Funktion aufnimmt und prüft, ob sie Schlüsselwortargumente hat…

def func_has_kwargs(func):
    return len(func.__defaults__) > 0

Mache Dir keine Sorgen über __defaults__, wenn Du es noch nicht gesehen hast – das Wichtigste ist hier, dass die Funktion eine andere Funktion als Argument nimmt, prüft, ob sie irgendwelche Schlüsselwortargumente hat (d.h. ob die Länge ihrer __default__-Eigenschaft größer als 0 ist), und wenn ja, True zurückgibt, ansonsten False.

Funktionen können auch neue Funktionen erstellen und zurückgeben. Hier ein Beispiel:

def make_divider_function(divisor):
    def divide_by_x(n):
        return n / divisor

    return divide_by_x

divide_by_3 = make_divider_function(3) divide_by_7 = make_divider_function(7)
print(divide_by_3(42)) # prints 14 print(divide_by_7(42)) # prints 6

Wenn man so etwas noch nicht gesehen hat, braucht man vielleicht ein paar Anläufe, um es zu verstehen. Gehen wir es Zeile für Zeile durch.

Wir beginnen mit der Erstellung einer Funktion namens make_divider_function. Diese Funktion nimmt eine Zahl als Argument und erstellt bei jedem Aufruf eine neue Funktion. Die Funktionen, die sie erstellt, teilen alle eine bestimmte Zahl durch eine Konstante, und diese Konstante wird durch das bestimmt, was man der ursprünglichen Funktion übergibt. Man kann sich die äußere Funktion wie eine „Fabrik“ vorstellen – sie erzeugt neue Funktionen, die Zahlen dividieren können.

Wir nutzen diese Fabrik, um zwei neue Funktionen zu erstellen. Wir erstellen eine Funktion, die eine beliebige Zahl durch 3 dividiert, und dann eine Funktion, die eine beliebige Zahl durch 7 dividiert. Und um zu zeigen, dass diese neuen Funktionen wie jede andere Funktion sind (trotz der seltsamen Art, wie wir sie erstellt haben), verwenden wir diese neuen Funktionen, um 42 durch 3 und dann durch 7 zu dividieren.

Es lohnt sich wirklich, das obige Beispiel durchzugehen, bis man sicher ist, dass man es verstanden hat.

Kehren wir zu unserem ursprünglichen Problem zurück. Wir haben unsere vier Funktionen zur Manipulation von Zeichenketten, die wir so sorgfältig ausgearbeitet haben, und wir müssen sie alle so ändern, dass sie auch ganze Zahlen akzeptieren. Es stellt sich heraus, dass wir eine neue Funktion brauchen – eine, die unsere bestehenden Funktionen als Eingaben nimmt und eine modifizierte Version von ihnen erstellt, die auf Ganzzahlen prüft. Wir brauchen eine Decorator-Funktion.

def accept_integers(func):     # definiert die Decorator Funktion,
    def new_func(s):           # die eine neue Funktion erzeugt.
        if isinstance(s, int): # Diese neue Funktion konvertiert jede ...
            s = str(s)         # ... Integer-Eingabe zu einem String.
        return func(s)         # Die neue Funktion ruft die original Funktion auf.
    return new_func            # Die neue Funktionnwird zurückgegeben.

Schauen wir uns genau an, was hier passiert. accept_integers ist unsere Dekoratorfunktion – sie nimmt eine Funktion als Eingabe und gibt eine andere Funktion als Ausgabe zurück. In ihrem Körper erstellt sie eine neue Funktion, die alles tun sollte, was die Eingabefunktion tut, aber mit einem zusätzlichen Schritt am Anfang. Wenn man sich den Körper dieser Funktion ansieht, kann man sehen, dass sie eine übergebene Zeichenkette daraufhin überprüft, ob es sich um eine ganze Zahl handelt, sie gegebenenfalls umwandelt und diese Zeichenkette dann an die ursprüngliche Funktion weitergibt.

Hier fehlt noch ein Schritt – wir müssen diesen Dekorator tatsächlich benutzen…

def accept_integers(func):
    def new_func(s):
        if isinstance(s, int):
            s = str(s)
        return func(s)
    return new_func


def make_palindrome(string):   """Makes a palindromic mirror of a string."""
    return string + string[::-1]

def add_credits(string):   """Adds the company's credits to the end of any string."""
    return f"{string} (string created by Pro String Inc.)"

def snake_to_camel(string):   """Converts a string in snake_case to camelCase."""     words = string.split("_")
    if len(words) > 1:        words = [words[0]] + [word.title() for word in words[1:]]
    return "".join(words)

def insert_commas(string, spacing=3):   """Inserts commas between every n characters."""   sections = [string[i: i + spacing] for i in range(0, len(string), spacing)]
  return ",".join(sections)

make_palindrome = accept_integers(make_palindrome) add_credits = accept_integers(add_credits) snake_to_camel = accept_integers(snake_to_camel) insert_commas = accept_integers(insert_commas)

Die vier Zeilen am Ende des Codes sind die eigentliche Anwendung der Dekoratoren. Schauen wir uns an, was mit der Funktion make_palindrome passiert…

Eine Funktion namens make_palindrome wird auf die übliche Art und Weise erstellt, die eine Zeichenkette entgegennimmt und eine Palindrom-Version davon zurückgibt. Sie hat keine Möglichkeit zu prüfen, ob die übergebene Zeichenkette eine ganze Zahl ist.

Diese make_palindrome-Funktion wird an die accept_integers-Dekoratorfunktion übergeben.

Die Funktion accept_integers erstellt eine neue Funktion, die ebenfalls ein Argument annimmt. Sie prüft dann, ob das Argument eine Ganzzahl ist, konvertiert es in einen String, wenn dies der Fall ist, und übergibt es an die ursprüngliche make_palindrome-Funktion. Beachte, dass an dieser Stelle weder die Typüberprüfung noch die make_palindrome-Funktion ausgeführt wird. Es wird lediglich definiert, wie die nächste Funktion arbeiten soll.

Die neue Funktion – eine erweiterte Version von make_palindrome – wird zurückgegeben. Da wir wollen, dass die neue Funktion denselben Namen wie die alte hat, weisen wir sie einer Variablen mit demselben Namen zu, so dass make_palindrome immer noch der Name der Funktion ist – obwohl wir sie auch anders hätten nennen können. Beachte, dass Du die Funktion irgendetwas zuweisen musst! Der Dekorator erzeugt eine neue Funktion, er verändert die alte nicht wirklich. Es ist die Zuweisung der neuen Funktion an denselben alten Namen, die den Effekt hat.

Abschließend sei darauf hingewiesen, dass die obige Syntax zwar vollkommen gültig und auch lesbar ist, Python aber eine Abkürzung in Form des @-Symbols bietet. Man kann @accept_integers vor jede Funktion setzen, um sie zu dekorieren.

def accept_integers(func):
    def new_func(s):
        if isinstance(s, int):
            s = str(s)
        return func(s)
    return new_func


@accept_integers def make_palindrome(string):     """Makes a palindromic mirror of a string."""
    return string + string[::-1]

@accept_integers def add_credits(string):     """Adds the company's credits to the end of any string."""
    return f"{string} (string created by Pro String Inc.)"

@accept_integers def snake_to_camel(string):     """Converts a string in snake_case to camelCase."""     words = string.split("_")
    if len(words) > 1:        words = [words[0]] + [word.title() for word in words[1:]]
    return "".join(words)

@accept_integers def insert_commas(string, spacing=3):   """Inserts commas between every n characters."""   sections = [string[i: i + spacing] for i in range(0, len(string), spacing)]
  return ",".join(sections)

So sieht man Dekoratoren normalerweise „in freier Wildbahn“ – aber denke daran, dass es nur eine weitere Möglichkeit ist, eine Funktion an eine andere Funktion zu übergeben. Unter der Haube, wenn Python das @-Symbol sieht, übernimmt es den Aufruf des Dekorators.

Wenn man sich mit den weniger anfängerfreundlichen Teilen von Python (oder jeder anderen Programmiersprache) befasst, ist man versucht, einen Schritt zurückzutreten und sich zu sagen, dass man auch ohne dies auskommt, und weiterzumachen wie bisher. Und am Anfang ist das auch in Ordnung. Aber letztendlich bedeutet das, zu akzeptieren, dass man so gut geworden ist, wie man jemals werden wird. Mein Rat für Decorator, und für alle neuen Programmierfunktionen, ist, zuerst zu lernen, die Situationen zu erkennen, in denen man diese Funktion benutzen würde – die Probleme, die sie löst, und die Schmerzen, die entstehen, wenn man sie nicht benutzt – und sich dann darum zu kümmern, zu lernen wie sie funktioniert.

Und wie immer ist der einzige Weg, es wirklich zu verstehen, der, es selbst zu programmieren.

(Original-Artikel von Sam Ireland)