Definieren Sie einfache Decorator-Funktionen

PythonPythonBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

In diesem Lab werden Sie lernen, was Decorators sind und wie sie in Python funktionieren. Decorators sind eine leistungsstarke Funktion, die es Ihnen ermöglicht, das Verhalten von Funktionen zu ändern, ohne den Quellcode zu verändern. Sie werden in Python-Frameworks und -Bibliotheken weit verbreitet eingesetzt.

Sie werden auch lernen, einen einfachen Logging-Decorator zu erstellen und einen komplexeren für die Funktionsvalidierung zu implementieren. Die Dateien, die in diesem Lab verwendet werden, sind logcall.py, sample.py und validate.py, wobei validate.py modifiziert wird.

Erstellen Ihres ersten Decorators

Was sind Decorators?

In Python sind Decorators eine besondere Syntax, die für Anfänger sehr nützlich sein kann. Sie ermöglichen es Ihnen, das Verhalten von Funktionen oder Methoden zu ändern. Stellen Sie sich einen Decorator als eine Funktion vor, die eine andere Funktion als Eingabe nimmt. Sie gibt dann eine neue Funktion zurück. Diese neue Funktion erweitert oder ändert oft das Verhalten der ursprünglichen Funktion.

Decorators werden mit dem @-Symbol angewendet. Sie platzieren dieses Symbol gefolgt vom Namen des Decorators direkt über einer Funktionsdefinition. Dies ist eine einfache Möglichkeit, Python mitzuteilen, dass Sie den Decorator auf diese bestimmte Funktion anwenden möchten.

Erstellen eines einfachen Logging-Decorators

Lassen Sie uns einen einfachen Decorator erstellen, der Informationen protokolliert, wenn eine Funktion aufgerufen wird. Logging ist eine häufige Aufgabe in realen Anwendungen, und die Verwendung eines Decorators dafür ist eine gute Möglichkeit, zu verstehen, wie sie funktionieren.

  1. Öffnen Sie zunächst den VSCode-Editor. Erstellen Sie im Verzeichnis /home/labex/project eine neue Datei mit dem Namen logcall.py. In dieser Datei wird unsere Decorator-Funktion gespeichert.

  2. Fügen Sie den folgenden Code in logcall.py ein:

## logcall.py

def logged(func):
    print('Adding logging to', func.__name__)
    def wrapper(*args, **kwargs):
        print('Calling', func.__name__)
        return func(*args, **kwargs)
    return wrapper

Lassen Sie uns analysieren, was dieser Code tut:

  • Die logged-Funktion ist unser Decorator. Sie nimmt eine andere Funktion, die wir func nennen, als Argument. Diese func ist die Funktion, für die wir Logging hinzufügen möchten.
  • Wenn der Decorator auf eine Funktion angewendet wird, wird eine Nachricht ausgegeben. Diese Nachricht teilt uns mit, dass Logging für die Funktion mit dem angegebenen Namen hinzugefügt wird.
  • Innerhalb der logged-Funktion definieren wir eine innere Funktion namens wrapper. Diese wrapper-Funktion wird die ursprüngliche Funktion ersetzen.
    • Wenn die dekorierte Funktion aufgerufen wird, gibt die wrapper-Funktion eine Nachricht aus, die besagt, dass die Funktion aufgerufen wird.
    • Sie ruft dann die ursprüngliche Funktion (func) mit allen Argumenten auf, die ihr übergeben wurden. Die *args und **kwargs werden verwendet, um eine beliebige Anzahl von Positions- und Schlüsselwortargumenten zu akzeptieren.
    • Schließlich gibt sie das Ergebnis der ursprünglichen Funktion zurück.
  • Die logged-Funktion gibt die wrapper-Funktion zurück. Diese wrapper-Funktion wird jetzt anstelle der ursprünglichen Funktion verwendet und fügt die Logging-Funktionalität hinzu.

Verwenden des Decorators

  1. Erstellen Sie nun im gleichen Verzeichnis (/home/labex/project) eine weitere Datei mit dem Namen sample.py mit dem folgenden Code:
## sample.py

from logcall import logged

@logged
def add(x, y):
    return x + y

@logged
def sub(x, y):
    return x - y

Die @logged-Syntax ist hier sehr wichtig. Sie teilt Python mit, den logged-Decorator auf die add- und sub-Funktionen anzuwenden. Wenn also diese Funktionen aufgerufen werden, wird die durch den Decorator hinzugefügte Logging-Funktionalität ausgeführt.

Testen des Decorators

  1. Um Ihren Decorator zu testen, öffnen Sie ein Terminal in VSCode. Ändern Sie zunächst das Verzeichnis in das Projektverzeichnis mit dem folgenden Befehl:
cd /home/labex/project

Starten Sie dann den Python-Interpreter:

python3
  1. Importieren Sie im Python-Interpreter das sample-Modul und testen Sie die dekorierten Funktionen:
>>> import sample
Adding logging to add
Adding logging to sub
>>> sample.add(3, 4)
Calling add
7
>>> sample.sub(2, 3)
Calling sub
-1
>>> exit()

Beachten Sie, dass beim Importieren des sample-Moduls die Nachrichten "Adding logging to..." ausgegeben werden. Dies liegt daran, dass der Decorator angewendet wird, wenn das Modul importiert wird. Jedes Mal, wenn Sie eine der dekorierten Funktionen aufrufen, wird die "Calling..."-Nachricht ausgegeben. Dies zeigt, dass der Decorator wie erwartet funktioniert.

Dieser einfache Decorator demonstriert das grundlegende Konzept von Decorators. Er umhüllt die ursprüngliche Funktion mit zusätzlicher Funktionalität (in diesem Fall Logging), ohne den Code der ursprünglichen Funktion zu ändern. Dies ist eine leistungsstarke Funktion in Python, die Sie in vielen verschiedenen Szenarien nutzen können.

✨ Lösung prüfen und üben

Erstellen eines Validierungs-Decorators

In diesem Schritt werden wir einen praktischeren Decorator erstellen. Ein Decorator in Python ist eine spezielle Art von Funktion, die das Verhalten einer anderen Funktion ändern kann. Der von uns zu erstellende Decorator wird die Funktionsargumente anhand von Typannotationen validieren. Typannotationen sind eine Möglichkeit, die erwarteten Datentypen der Argumente und des Rückgabewerts einer Funktion anzugeben. Dies ist ein häufiger Anwendungsfall in realen Anwendungen, da es hilft sicherzustellen, dass Funktionen die richtigen Eingabetypen erhalten, was viele Fehler vermeiden kann.

Verständnis der Validierungsklassen

Wir haben bereits eine Datei namens validate.py für Sie erstellt, die einige Validierungsklassen enthält. Validierungsklassen werden verwendet, um zu prüfen, ob ein Wert bestimmte Kriterien erfüllt. Um sehen zu können, was in dieser Datei enthalten ist, müssen Sie sie im VSCode-Editor öffnen. Sie können dies tun, indem Sie die folgenden Befehle im Terminal ausführen:

cd /home/labex/project
code validate.py

Die Datei enthält drei Klassen:

  1. Validator - Dies ist eine Basisklasse. Eine Basisklasse bietet ein allgemeines Gerüst oder eine Struktur, von der andere Klassen erben können. In diesem Fall bietet sie die grundlegende Struktur für die Validierung.
  2. Integer - Diese Validator-Klasse wird verwendet, um sicherzustellen, dass ein Wert eine Ganzzahl ist. Wenn Sie einem Funktionsaufruf, der diesen Validator verwendet, einen Wert übergeben, der keine Ganzzahl ist, wird ein Fehler ausgelöst.
  3. PositiveInteger - Diese Validator-Klasse stellt sicher, dass ein Wert eine positive Ganzzahl ist. Wenn Sie also eine negative Ganzzahl oder Null übergeben, wird ebenfalls ein Fehler ausgelöst.

Hinzufügen des Validierungs-Decorators

Jetzt werden wir der Datei validate.py eine Decorator-Funktion namens validated hinzufügen. Dieser Decorator wird mehrere wichtige Aufgaben ausführen:

  1. Er wird die Typannotationen einer Funktion untersuchen. Typannotationen sind wie kleine Hinweise, die uns sagen, welche Art von Daten die Funktion erwartet.
  2. Er wird die an die Funktion übergebenen Argumente anhand dieser Typannotationen validieren. Das bedeutet, er wird prüfen, ob die an die Funktion übergebenen Werte vom richtigen Typ sind.
  3. Er wird auch den Rückgabewert der Funktion anhand seiner Annotation validieren. Somit wird sichergestellt, dass die Funktion den Datentyp zurückgibt, den sie zurückgeben soll.
  4. Wenn die Validierung fehlschlägt, wird er informative Fehlermeldungen ausgeben. Diese Meldungen werden Ihnen genau sagen, was schief gelaufen ist, z. B. welches Argument den falschen Typ hatte.

Fügen Sie den folgenden Code ans Ende der Datei validate.py hinzu:

## Add to validate.py

import inspect
import functools

def validated(func):
    sig = inspect.signature(func)

    print(f'Validating {func.__name__} {sig}')

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        ## Bind arguments to the signature
        bound = sig.bind(*args, **kwargs)
        errors = []

        ## Validate each argument
        for name, value in bound.arguments.items():
            if name in sig.parameters:
                param = sig.parameters[name]
                if param.annotation != inspect.Parameter.empty:
                    try:
                        ## Create an instance of the validator and validate the value
                        if isinstance(param.annotation, type) and issubclass(param.annotation, Validator):
                            validator = param.annotation()
                            bound.arguments[name] = validator.validate(value)
                    except Exception as e:
                        errors.append(f'    {name}: {e}')

        ## If validation errors, raise an exception
        if errors:
            raise TypeError('Bad Arguments\n' + '\n'.join(errors))

        ## Call the function
        result = func(*bound.args, **bound.kwargs)

        ## Validate the return value
        if sig.return_annotation != inspect.Signature.empty:
            try:
                if isinstance(sig.return_annotation, type) and issubclass(sig.return_annotation, Validator):
                    validator = sig.return_annotation()
                    result = validator.validate(result)
            except Exception as e:
                raise TypeError(f'Bad return: {e}') from None

        return result

    return wrapper

Dieser Code verwendet das inspect-Modul von Python. Das inspect-Modul ermöglicht es uns, Informationen über lebende Objekte wie Funktionen zu erhalten. Hier verwenden wir es, um die Signatur der Funktion zu untersuchen und die Argumente anhand von Typannotationen zu validieren. Wir verwenden auch functools.wraps. Dies ist eine Hilfsfunktion, die die Metadaten der ursprünglichen Funktion, wie ihren Namen und ihre Docstring, beibehält. Metadaten sind wie zusätzliche Informationen über die Funktion, die uns helfen, zu verstehen, was sie tut.

Testen des Validierungs-Decorators

Lassen Sie uns eine Datei erstellen, um unseren Validierungs-Decorator zu testen. Wir werden eine neue Datei namens test_validate.py erstellen und den folgenden Code hinzufügen:

## test_validate.py

from validate import Integer, PositiveInteger, validated

@validated
def add(x: Integer, y: Integer) -> Integer:
    return x + y

@validated
def pow(x: Integer, y: Integer) -> Integer:
    return x ** y

## Test with a class
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price

    @validated
    def sell(self, nshares: PositiveInteger):
        self.shares -= nshares

Jetzt werden wir unseren Decorator im Python-Interpreter testen. Navigieren Sie zunächst in das Projektverzeichnis und starten Sie den Python-Interpreter, indem Sie die folgenden Befehle im Terminal ausführen:

cd /home/labex/project
python3

Dann können wir im Python-Interpreter den folgenden Code ausführen, um unseren Decorator zu testen:

>>> from test_validate import add, pow, Stock
Validating add (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating pow (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating sell (self, nshares: validate.PositiveInteger) -> <class 'inspect._empty'>
>>>
>>> ## Test with valid inputs
>>> add(2, 3)
5
>>>
>>> ## Test with invalid inputs
>>> add('2', '3')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 75, in wrapper
    raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
    x: Expected <class 'int'>
    y: Expected <class 'int'>
>>>
>>> ## Test valid power
>>> pow(2, 3)
8
>>>
>>> ## Test with negative exponent (produces non - integer result)
>>> pow(2, -1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 83, in wrapper
    raise TypeError(f'Bad return: {e}') from None
TypeError: Bad return: Expected <class 'int'>
>>>
>>> ## Test with a class
>>> s = Stock("GOOG", 100, 490.1)
>>> s.sell(50)
>>> s.shares
50
>>>
>>> ## Test with invalid shares
>>> s.sell(-10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 75, in wrapper
    raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
    nshares: Expected value > 0
>>> exit()

Wie Sie sehen können, hat unser validated-Decorator die Typüberprüfung von Funktionsargumenten und Rückgabewerten erfolgreich durchgeführt. Dies ist sehr nützlich, da es unseren Code robuster macht. Anstatt zuzulassen, dass Typfehler tiefer in den Code vordringen und schwer zu findende Fehler verursachen, fangen wir sie an den Funktionsgrenzen ab.

✨ Lösung prüfen und üben

Zusammenfassung

In diesem Lab haben Sie sich mit Decorators in Python vertraut gemacht, einschließlich ihrer Definition und ihres Funktionsprinzips. Sie haben auch gelernt, wie Sie einen einfachen Logging-Decorator erstellen, um Verhalten zu Funktionen hinzuzufügen, und einen komplexeren Decorator für die Validierung von Funktionsargumenten anhand von Typannotationen entwickelt. Darüber hinaus haben Sie gelernt, das inspect-Modul zur Analyse von Funktionssignaturen und functools.wraps zur Aufrechterhaltung von Funktionsmetadaten zu verwenden.

Decorators sind eine leistungsstarke Python-Funktion, die es ermöglicht, wartbareren und wiederverwendbaren Code zu schreiben. Sie werden häufig in Python-Frameworks und -Bibliotheken für Querschnittsfunktionen wie Logging, Zugriffskontrolle und Caching eingesetzt. Sie können diese Techniken nun in Ihren eigenen Python-Projekten anwenden, um sauberen und wartbaren Code zu schreiben.