Python Decorators verstehen

PythonPythonBeginner
Jetzt üben

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

Einleitung

In diesem Lab erhalten Sie ein umfassendes Verständnis von Decorators in Python, einem mächtigen Feature zur Modifikation oder Verbesserung von Funktionen und Methoden. Wir beginnen mit der Einführung des grundlegenden Konzepts von Decorators und erkunden deren grundlegende Verwendung anhand praktischer Beispiele.

Aufbauend auf dieser Grundlage lernen Sie, wie Sie functools.wraps effektiv nutzen, um wichtige Metadaten der dekorierten Funktion zu erhalten. Anschließend werden wir uns spezifischen Decorators wie dem property-Decorator widmen und dessen Rolle bei der Verwaltung des Attributzugriffs verstehen. Abschließend wird das Lab die Unterschiede zwischen Instanzmethoden, Klassenmethoden und statischen Methoden verdeutlichen und zeigen, wie Decorators in diesen Kontexten verwendet werden, um das Verhalten von Methoden innerhalb von Klassen zu steuern.

Einführung von Decorators und grundlegende Verwendung

In diesem Schritt führen wir das Konzept von Decorators in Python ein und lernen, wie man sie für die grundlegende Funktionsdekoration verwendet. Decorators sind ein mächtiges Feature, das es Ihnen ermöglicht, Funktionen oder Methoden zu modifizieren oder zu verbessern, ohne deren Quellcode zu ändern.

Ein Decorator ist im Wesentlichen eine Funktion, die eine andere Funktion als Argument nimmt und eine neue Funktion zurückgibt, die normalerweise das Verhalten der ursprünglichen Funktion erweitert.

Beginnen wir mit der Erstellung einer einfachen Funktion, die wir dekorieren möchten. Öffnen Sie das integrierte Terminal in der WebIDE und navigieren Sie zum Verzeichnis ~/project.

Erstellen Sie nun eine neue Python-Datei namens decorator_example.py im Verzeichnis ~/project über den Dateiexplorer der WebIDE. Öffnen Sie die Datei und fügen Sie den folgenden Code hinzu:

from datetime import datetime

def hello(name):
    print(f'Hello {name}')

## Rufen Sie die ursprüngliche Funktion auf
hello('Tom')

Speichern Sie die Datei. Führen Sie nun das Skript im Terminal aus:

python ~/project/decorator_example.py

Sie sollten die Ausgabe sehen:

Hello Tom

Erstellen wir nun eine einfache Decorator-Funktion, die das aktuelle Datum und die Uhrzeit ausgibt, bevor die dekorierte Funktion ausgeführt wird. Fügen Sie den folgenden Code zu decorator_example.py hinzu:

from datetime import datetime
from functools import wraps ## Dies werden wir später verwenden

def print_datetime(func):
    def wrapper(*args, **kw):
        print(datetime.now())
        func(*args, **kw)
    return wrapper

def hello(name):
    print(f'Hello {name}')

## Wenden Sie den Decorator manuell an
f = print_datetime(hello)
f('Tom')

## Rufen Sie die ursprüngliche Funktion erneut auf, um den Unterschied zu sehen
hello('Jerry')

Speichern Sie die Datei und führen Sie sie erneut aus:

python ~/project/decorator_example.py

Sie sollten eine Ausgabe ähnlich dieser sehen:

Hello Tom
<current_datetime>
Hello Tom
Hello Jerry

Beachten Sie, dass beim Aufruf von f('Tom') die wrapper-Funktion des print_datetime-Decorators ausgeführt wurde, die zuerst das aktuelle Datum und die Uhrzeit ausgab und dann die ursprüngliche hello-Funktion aufrief. Als wir hello('Jerry') direkt aufriefen, wurde nur die ursprüngliche hello-Funktion ausgeführt.

Python bietet eine bequemere Syntax für die Anwendung von Decorators mit dem @-Symbol. Modifizieren wir den Code, um die @-Syntax zu verwenden. Aktualisieren Sie decorator_example.py wie folgt:

from datetime import datetime
from functools import wraps

def print_datetime(func):
    def wrapper(*args, **kw):
        print(datetime.now())
        func(*args, **kw)
    return wrapper

@print_datetime
def hello(name):
    """Dies ist der Docstring für die hello-Funktion."""
    print(f'Hello {name}')

## Rufen Sie die dekorierte Funktion auf
hello('Tom')

## Überprüfen Sie den Namen der dekorierten Funktion
print(f"Function name: {hello.__name__}")

Speichern Sie die Datei und führen Sie sie aus:

python ~/project/decorator_example.py

Die Ausgabe sollte ähnlich dieser sein:

<current_datetime>
Hello Tom
Function name: wrapper

Wie Sie sehen können, ist die Verwendung der @print_datetime-Syntax äquivalent zu hello = print_datetime(hello). Die dekorierte Funktion hello ist nun tatsächlich die wrapper-Funktion, die vom Decorator zurückgegeben wird. Deshalb zeigt hello.__name__ 'wrapper' an.

Im nächsten Schritt lernen wir, wie wir die Metadaten der ursprünglichen Funktion (wie ihren Namen und Docstring) bei der Verwendung von Decorators mithilfe von functools.wraps erhalten.

Verwendung von functools.wraps mit Decorators

Im vorherigen Schritt haben wir gesehen, dass sich beim Dekorieren einer Funktion ihr __name__-Attribut in den Namen der Wrapper-Funktion ändert. Dies kann problematisch sein, insbesondere beim Debugging oder bei der Verwendung von Tools, die auf Funktionsmetadaten angewiesen sind.

Das functools-Modul in Python stellt den wraps-Decorator bereit, der speziell zur Behebung dieses Problems entwickelt wurde. Der wraps-Decorator wird innerhalb Ihrer Decorator-Funktion verwendet, um den Namen, den Docstring und andere Metadaten der ursprünglichen Funktion zu erhalten.

Lassen Sie uns unseren print_datetime-Decorator modifizieren, um functools.wraps zu verwenden. Öffnen Sie die Datei ~/project/decorator_example.py in der WebIDE und aktualisieren Sie die print_datetime-Funktion wie folgt:

from datetime import datetime
from functools import wraps

def print_datetime(func):
    @wraps(func) ## Verwenden Sie hier @wraps(func)
    def wrapper(*args, **kw):
        print(datetime.now())
        func(*args, **kw)
    return wrapper

@print_datetime
def hello(name):
    """Dies ist der Docstring für die hello-Funktion."""
    print(f'Hello {name}')

## Rufen Sie die dekorierte Funktion auf
hello('Tom')

## Überprüfen Sie den Namen und den Docstring der dekorierten Funktion
print(f"Function name: {hello.__name__}")
print(f"Function docstring: {hello.__doc__}")

Speichern Sie die Datei und führen Sie sie im Terminal aus:

python ~/project/decorator_example.py

Die Ausgabe sollte nun ähnlich dieser sein:

<current_datetime>
Hello Tom
Function name: hello
Function docstring: This is the docstring for the hello function.

Beachten Sie, dass nach dem Hinzufügen von @wraps(func) innerhalb des print_datetime-Decorators der __name__ der dekorierten hello-Funktion nun korrekt 'hello' ist und ihr __doc__-Attribut auch den ursprünglichen Docstring enthält.

Die Verwendung von functools.wraps ist eine bewährte Methode bei der Erstellung eigener Decorators, da sie dazu beiträgt, die Integrität der Metadaten der dekorierten Funktion zu wahren.

Erkundung des property-Decorators

In diesem Schritt werden wir den property-Decorator untersuchen, der häufig in Klassen verwendet wird, um eine bequeme Möglichkeit zum Zugreifen auf und Ändern von Attributen zu bieten, während gleichzeitig die zugrunde liegende Logik (wie Validierung oder Berechnung) ermöglicht wird.

Der property-Decorator ermöglicht es Ihnen, Methoden für das Abrufen, Setzen und Löschen eines Attributs zu definieren, aber darauf zuzugreifen, als wären es einfache Attribute. Dies wird oft verwendet, um "verwaltete Attribute" oder "Properties" zu erstellen.

Erstellen wir eine einfache Klasse Circle, die den property-Decorator für ihr radius-Attribut verwendet. Öffnen Sie das integrierte Terminal und erstellen Sie eine neue Python-Datei namens property_example.py im Verzeichnis ~/project. Fügen Sie den folgenden Code hinzu:

import math

class Circle:
    def __init__(self, radius):
        self._radius = radius ## Verwenden Sie ein privates Attribut

    @property
    def radius(self):
        """Ruft den Radius des Kreises ab."""
        print("Getting radius...")
        return self._radius

    @radius.setter
    def radius(self, value):
        """Setzt den Radius des Kreises."""
        print("Setting radius...")
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        """Berechnet die Fläche des Kreises."""
        print("Calculating area...")
        return math.pi * self._radius**2

## Erstellen Sie eine Instanz der Circle-Klasse
my_circle = Circle(5)

## Greifen Sie über die Property auf den Radius zu
print(f"Initial radius: {my_circle.radius}")

## Ändern Sie den Radius über die Property
my_circle.radius = 10
print(f"New radius: {my_circle.radius}")

## Greifen Sie über die Property auf die Fläche zu
print(f"Area: {my_circle.area}")

## Versuchen Sie, einen negativen Radius zu setzen (dies sollte einen Fehler auslösen)
try:
    my_circle.radius = -2
except ValueError as e:
    print(f"Error setting radius: {e}")

Speichern Sie die Datei und führen Sie sie im Terminal aus:

python ~/project/property_example.py

Die Ausgabe sollte ähnlich dieser sein:

Getting radius...
Initial radius: 5
Setting radius...
Getting radius...
New radius: 10
Calculating area...
Area: 314.1592653589793
Setting radius...
Error setting radius: Radius cannot be negative

In diesem Beispiel:

  • Wir haben ein privates Attribut _radius definiert, um den tatsächlichen Radiuswert zu speichern.
  • Der @property-Decorator wird auf die radius-Methode angewendet, um zu definieren, wie der Wert der radius-Property abgerufen wird. Wenn Sie auf my_circle.radius zugreifen, wird diese Methode aufgerufen.
  • Der @radius.setter-Decorator wird auf eine andere Methode namens radius angewendet, um zu definieren, wie der Wert der radius-Property gesetzt wird. Wenn Sie einen Wert an my_circle.radius zuweisen, wird diese Methode aufgerufen. Wir haben hier eine Prüfung hinzugefügt, um das Setzen eines negativen Radius zu verhindern.
  • Wir haben auch eine area-Property mit @property definiert. Diese Property berechnet die Fläche bei Bedarf, wenn darauf zugegriffen wird.

Der property-Decorator ermöglicht es Ihnen zu steuern, wie auf Attribute zugegriffen und wie sie geändert werden, und bietet eine saubere und intuitive Schnittstelle für Benutzer Ihrer Klasse.

Verständnis von Klassenmethoden und statischen Methoden

In diesem Schritt lernen wir zwei spezielle Arten von Methoden in Python-Klassen kennen: Klassenmethoden und statische Methoden. Diese Methoden unterscheiden sich von regulären Instanzmethoden, da sie mit der Klasse selbst verbunden sind und nicht mit einer bestimmten Instanz der Klasse.

Klassenmethoden:

Klassenmethoden werden mit dem @classmethod-Decorator definiert. Der erste Parameter einer Klassenmethode wird konventionell cls genannt, der sich auf das Klassenobjekt selbst bezieht. Klassenmethoden werden oft als alternative Konstruktoren oder zum Zugreifen auf oder Ändern von klassenbezogenen Attributen verwendet.

Erstellen wir eine Klasse MyClass mit einer Klassenmethode. Öffnen Sie das integrierte Terminal und erstellen Sie eine neue Python-Datei namens class_static_methods.py im Verzeichnis ~/project. Fügen Sie den folgenden Code hinzu:

class MyClass:
    class_variable = "I am a class variable"

    def __init__(self, instance_variable):
        self.instance_variable = instance_variable

    def instance_method(self):
        print(f"Instance method called. Instance variable: {self.instance_variable}")
        print(f"Accessing class variable from instance method: {self.class_variable}")

    @classmethod
    def class_method(cls):
        print(f"Class method called. Class variable: {cls.class_variable}")
        print(f"Accessing class name from class method: {cls.__name__}")
        ## Sie können auch auf Instanzvariablen zugreifen, aber das ist weniger üblich
        ## print(f"Accessing instance variable from class method: {cls().instance_variable}") ## Dies erstellt eine neue Instanz

## Direkter Zugriff auf die Klassenvariable
print(f"Accessing class variable directly: {MyClass.class_variable}")

## Aufrufen der Klassenmethode über die Klasse
MyClass.class_method()

## Erstellen einer Instanz
my_instance = MyClass("I am an instance variable")

## Aufrufen der Klassenmethode über eine Instanz (funktioniert auch)
my_instance.class_method()

Speichern Sie die Datei und führen Sie sie aus:

python ~/project/class_static_methods.py

Die Ausgabe sollte sein:

Accessing class variable directly: I am a class variable
Class method called. Class variable: I am a class variable
Accessing class name from class method: MyClass
Class method called. Class variable: I am a class variable
Accessing class name from class method: MyClass

Wie Sie sehen können, empfängt die Klassenmethode die Klasse selbst (cls) als erstes Argument und kann über cls.class_variable auf Klassenvariablen zugreifen. Sie kann entweder über den Klassennamen (MyClass.class_method()) oder eine Instanz (my_instance.class_method()) aufgerufen werden.

Statische Methoden:

Statische Methoden werden mit dem @staticmethod-Decorator definiert. Sie erhalten kein implizites erstes Argument (weder self noch cls). Statische Methoden sind im Wesentlichen reguläre Funktionen, die innerhalb einer Klasse platziert werden, weil sie eine logische Verbindung zur Klasse haben, aber sie benötigen keinen Zugriff auf instanzspezifische oder klassenspezifische Daten.

Fügen wir unserer MyClass eine statische Methode hinzu. Aktualisieren Sie class_static_methods.py mit Folgendem:

class MyClass:
    class_variable = "I am a class variable"

    def __init__(self, instance_variable):
        self.instance_variable = instance_variable

    def instance_method(self):
        print(f"Instance method called. Instance variable: {self.instance_variable}")
        print(f"Accessing class variable from instance method: {self.class_variable}")

    @classmethod
    def class_method(cls):
        print(f"Class method called. Class variable: {cls.class_variable}")
        print(f"Accessing class name from class method: {cls.__name__}")

    @staticmethod
    def static_method(x, y):
        print("Static method called.")
        return x + y

## Direkter Zugriff auf die Klassenvariable
print(f"Accessing class variable directly: {MyClass.class_variable}")

## Aufrufen der Klassenmethode über die Klasse
MyClass.class_method()

## Erstellen einer Instanz
my_instance = MyClass("I am an instance variable")

## Aufrufen der Klassenmethode über eine Instanz (funktioniert auch)
my_instance.class_method()

## Aufrufen der statischen Methode über die Klasse
result_class = MyClass.static_method(5, 3)
print(f"Result of static method (called from class): {result_class}")

## Aufrufen der statischen Methode über eine Instanz (funktioniert auch)
result_instance = my_instance.static_method(10, 2)
print(f"Result of static method (called from instance): {result_instance}")

Speichern Sie die Datei und führen Sie sie aus:

python ~/project/class_static_methods.py

Die Ausgabe sollte sein:

Accessing class variable directly: I am a class variable
Class method called. Class variable: I am a class variable
Accessing class name from class method: MyClass
Class method called. Class variable: I am a class variable
Accessing class name from class method: MyClass
Static method called.
Result of static method (called from class): 8
Static method called.
Result of static method (called from instance): 12

Die statische Methode static_method nimmt zwei Argumente x und y entgegen und gibt deren Summe zurück. Sie greift nicht auf self oder cls zu. Sie kann entweder über den Klassennamen oder eine Instanz aufgerufen werden.

Im nächsten Schritt fassen wir die Hauptunterschiede zwischen Instanzmethoden, Klassenmethoden und statischen Methoden zusammen.

Unterscheidung zwischen Instanz-, Klassen- und statischen Methoden

In den vorherigen Schritten haben wir Beispiele für Instanzmethoden, Klassenmethoden und statische Methoden gesehen. Das Verständnis der Unterschiede zwischen ihnen ist entscheidend für effektive objektorientierte Programmierung in Python.

Hier ist eine Zusammenfassung der wichtigsten Unterscheidungen:

Merkmal Instanzmethode Klassenmethode Statische Methode
Decorator Keine (Standard) @classmethod @staticmethod
Erster Parameter self (Instanz) cls (Klasse) Keine
Datenzugriff Instanzdaten (self.) Klassendaten (cls.) Weder Instanz- noch Klassendaten direkt
Anwendungsfall Operationen auf Instanzstatus Operationen auf Klassenstatus, alternative Konstruktoren Hilfsfunktionen, die sich auf die Klasse beziehen, aber nicht vom Instanz- oder Klassenstatus abhängen
Aufgerufen von Instanz oder Klasse Instanz oder Klasse Instanz oder Klasse

Lassen Sie uns unser MyClass-Beispiel aus dem vorherigen Schritt noch einmal aufgreifen und eine Instanzmethode hinzufügen, um die Unterschiede bei der Art und Weise, wie sie aufgerufen werden und auf welche Daten sie zugreifen können, zu demonstrieren. Öffnen Sie ~/project/class_static_methods.py und stellen Sie sicher, dass es den folgenden Inhalt hat:

class MyClass:
    class_variable = "I am a class variable"

    def __init__(self, instance_variable):
        self.instance_variable = instance_variable

    ## Instanzmethode
    def instance_method(self):
        print("\n--- Inside Instance Method ---")
        print(f"Accessing instance variable: {self.instance_variable}")
        print(f"Accessing class variable: {self.class_variable}")
        ## Kann Klassen- und statische Methoden aus der Instanzmethode aufrufen
        self.class_method()
        print(f"Calling static method from instance method: {self.static_method(1, 1)}")
        print("--- End Instance Method ---")


    ## Klassenmethode
    @classmethod
    def class_method(cls):
        print("\n--- Inside Class Method ---")
        print(f"Accessing class variable: {cls.class_variable}")
        print(f"Accessing class name: {cls.__name__}")
        ## Kann nicht direkt auf Instanzvariablen zugreifen, ohne eine Instanz zu erstellen
        ## print(f"Attempting to access instance variable: {cls().instance_variable}") ## Dies würde eine neue Instanz erstellen
        print("--- End Class Method ---")


    ## Statische Methode
    @staticmethod
    def static_method(x, y):
        print("\n--- Inside Static Method ---")
        print("Static method called with arguments:", x, y)
        ## Kann nicht direkt auf Instanz- oder Klassenvariablen zugreifen
        ## print(f"Attempting to access class variable: {MyClass.class_variable}") ## Dies funktioniert, aber nicht über self oder cls
        print("--- End Static Method ---")
        return x + y

## Erstellen einer Instanz
my_instance = MyClass("Instance Data")

## Aufrufen der Instanzmethode
my_instance.instance_method()

## Aufrufen der Klassenmethode über die Klasse
MyClass.class_method()

## Aufrufen der statischen Methode über die Klasse
MyClass.static_method(10, 20)

Speichern Sie die Datei und führen Sie sie aus:

python ~/project/class_static_methods.py

Die Ausgabe zeigt deutlich, welche Methode aufgerufen wird und auf welche Daten sie zugreifen kann:

--- Inside Instance Method ---
Accessing instance variable: Instance Data
Accessing class variable: I am a class variable

--- Inside Class Method ---
Accessing class variable: I am a class variable
Accessing class name: MyClass
--- End Class Method ---

--- Inside Static Method ---
Static method called with arguments: 1 1
--- End Static Method ---
Calling static method from instance method: 2
--- End Instance Method ---

--- Inside Class Method ---
Accessing class variable: I am a class variable
Accessing class name: MyClass
--- End Class Method ---

--- Inside Static Method ---
Static method called with arguments: 10 20
--- End Static Method ---

Wichtige Erkenntnisse:

  • Instanzmethoden sind der häufigste Typ. Sie arbeiten mit den Daten der Instanz und können sowohl auf Instanz- als auch auf Klassenvariablen zugreifen.
  • Klassenmethoden arbeiten mit den Daten der Klasse und erhalten das Klassenobjekt als erstes Argument. Sie sind nützlich für Factory-Methoden oder Operationen, die klassenbezogenen Status beinhalten.
  • Statische Methoden sind unabhängig von der Instanz und der Klasse. Sie sind wie reguläre Funktionen, werden aber aus organisatorischen Gründen innerhalb einer Klasse gruppiert. Sie erhalten weder self noch cls.

Die Wahl des richtigen Methodentyps hängt davon ab, ob die Methode auf instanzspezifische Daten, klassenspezifische Daten oder keine von beiden zugreifen muss.

Zusammenfassung

In diesem Lab haben wir zunächst das Konzept von Decorators in Python eingeführt und verstanden, dass es sich um Funktionen handelt, die andere Funktionen modifizieren oder erweitern, ohne ihren Quellcode zu ändern. Wir haben die grundlegende Syntax für die Anwendung von Decorators gelernt und gesehen, wie ein einfacher Decorator verwendet werden kann, um Funktionalität hinzuzufügen, wie z. B. das Drucken des aktuellen Datums und der Uhrzeit vor der Ausführung der dekorierten Funktion. Wir haben auch die manuelle Anwendung von Decorators untersucht, um ihren zugrunde liegenden Mechanismus zu verstehen.

Aufbauend auf dem grundlegenden Verständnis werden wir die Verwendung von functools.wraps weiter untersuchen, um die Metadaten der ursprünglichen Funktion bei der Verwendung von Decorators zu erhalten. Anschließend werden wir uns mit spezifischen integrierten Decorators wie @property zur Verwaltung des Attributzugriffs und @classmethod sowie @staticmethod zur Definition verschiedener Methodentypen innerhalb einer Klasse befassen und deren Verwendung und Verhalten im Vergleich zu Instanzmethoden unterscheiden.