Definieren eines geeigneten aufrufbaren Objekts

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 lernen Sie etwas über aufrufbare Objekte in Python kennen. Ein aufrufbares Objekt kann wie eine Funktion mithilfe der Syntax object() aufgerufen werden. Während Python-Funktionen von Natur aus aufrufbar sind, können Sie benutzerdefinierte aufrufbare Objekte erstellen, indem Sie die Methode __call__ implementieren.

Sie werden auch lernen, wie Sie ein aufrufbares Objekt mithilfe der Methode __call__ implementieren und Funktionsannotationen mit aufrufbaren Objekten zur Parameterüberprüfung verwenden. Die Datei validate.py wird während dieses Labs geändert.

Verständnis von Validator-Klassen

In diesem Lab werden wir auf einer Reihe von Validator-Klassen aufbauen, um ein aufrufbares Objekt zu erstellen. Bevor wir mit dem Bau beginnen, ist es wichtig, die in der Datei validate.py bereitgestellten Validator-Klassen zu verstehen. Diese Klassen helfen uns bei der Typüberprüfung, was ein entscheidender Bestandteil dafür ist, dass unser Code wie erwartet funktioniert.

Lassen Sie uns beginnen, indem Sie die Datei validate.py in der WebIDE öffnen. Diese Datei enthält den Code für die Validator-Klassen, die wir verwenden werden. Um sie zu öffnen, führen Sie den folgenden Befehl im Terminal aus:

code /home/labex/project/validate.py

Sobald Sie die Datei geöffnet haben, werden Sie feststellen, dass sie mehrere Klassen enthält. Hier ist eine kurze Übersicht darüber, was jede Klasse macht:

  1. Validator: Dies ist eine Basisklasse. Sie hat eine Methode check, die derzeit jedoch nichts tut. Sie dient als Ausgangspunkt für die anderen Validator-Klassen.
  2. Typed: Dies ist eine Unterklasse von Validator. Ihre Hauptaufgabe besteht darin, zu überprüfen, ob ein Wert einen bestimmten Typ hat.
  3. Integer, Float und String: Dies sind spezifische Typ-Validatoren, die von Typed erben. Sie sind darauf ausgelegt, zu überprüfen, ob ein Wert eine Ganzzahl, eine Fließkommazahl oder eine Zeichenkette ist.

Jetzt sehen wir uns an, wie diese Validator-Klassen in der Praxis funktionieren. Wir werden eine neue Datei namens test.py erstellen, um sie zu testen. Um diese Datei zu erstellen und zu öffnen, führen Sie den folgenden Befehl aus:

code /home/labex/project/test.py

Sobald die Datei test.py geöffnet ist, fügen Sie den folgenden Code hinzu. Dieser Code wird die Integer- und String-Validatoren testen:

from validate import Integer, String, Float

## Test Integer validator
print("Testing Integer validator:")
try:
    Integer.check(42)
    print("✓ Integer check passed for 42")
except TypeError as e:
    print(f"✗ Error: {e}")

try:
    Integer.check("Hello")
    print("✗ Integer check incorrectly passed for 'Hello'")
except TypeError as e:
    print(f"✓ Correctly raised error: {e}")

## Test String validator
print("\nTesting String validator:")
try:
    String.check("Hello")
    print("✓ String check passed for 'Hello'")
except TypeError as e:
    print(f"✗ Error: {e}")

In diesem Code importieren wir zunächst die Integer-, String- und Float-Validatoren aus der Datei validate.py. Dann testen wir den Integer-Validator, indem wir versuchen, einen ganzzahligen Wert (42) und einen Zeichenkettenwert ("Hello") zu überprüfen. Wenn die Überprüfung für die Ganzzahl erfolgreich ist, geben wir eine Erfolgsmeldung aus. Wenn die Überprüfung für die Zeichenkette fehlerhaft erfolgreich ist, geben wir eine Fehlermeldung aus. Wenn die Überprüfung für die Zeichenkette korrekt einen TypeError auslöst, geben wir eine Erfolgsmeldung aus. Wir führen einen ähnlichen Test für den String-Validator durch.

Nachdem Sie den Code hinzugefügt haben, führen Sie die Testdatei mit dem folgenden Befehl aus:

python3 /home/labex/project/test.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Testing Integer validator:
✓ Integer check passed for 42
✓ Correctly raised error: Expected <class 'int'>

Testing String validator:
✓ String check passed for 'Hello'

Wie Sie sehen können, ermöglichen uns diese Validator-Klassen, die Typüberprüfung einfach durchzuführen. Beispielsweise wird beim Aufruf von Integer.check(x) ein TypeError ausgelöst, wenn x keine Ganzzahl ist.

Jetzt denken wir über ein praktisches Szenario nach. Angenommen, wir haben eine Funktion, die erfordert, dass ihre Argumente bestimmte Typen haben. Hier ist ein Beispiel für eine solche Funktion:

def add(x, y):
    Integer.check(x)  ## Make sure x is an integer
    Integer.check(y)  ## Make sure y is an integer
    return x + y

Diese Funktion funktioniert, aber es gibt ein Problem. Wir müssen die Validator-Überprüfungen manuell hinzufügen, jedes Mal wenn wir die Typüberprüfung verwenden möchten. Dies kann zeitaufwendig und fehleranfällig sein, insbesondere bei größeren Funktionen oder Projekten.

In den nächsten Schritten werden wir dieses Problem lösen, indem wir ein aufrufbares Objekt erstellen. Dieses Objekt kann diese Typüberprüfungen automatisch basierend auf Funktionsannotationen anwenden. Auf diese Weise müssen wir die Überprüfungen nicht jedes Mal manuell hinzufügen.

Erstellen eines einfachen aufrufbaren Objekts

In Python ist ein aufrufbares Objekt ein Objekt, das genauso wie eine Funktion verwendet werden kann. Sie können sich vorstellen, dass Sie es "aufrufen" können, indem Sie es mit Klammern versehen, ähnlich wie Sie eine normale Funktion aufrufen. Um eine Python-Klasse wie ein aufrufbares Objekt zu machen, müssen wir eine spezielle Methode namens __call__ implementieren. Diese Methode wird automatisch aufgerufen, wenn Sie das Objekt mit Klammern verwenden, genau wie wenn Sie eine Funktion aufrufen.

Lassen Sie uns beginnen, indem wir die Datei validate.py ändern. Wir werden dieser Datei eine neue Klasse namens ValidatedFunction hinzufügen, und diese Klasse wird unser aufrufbares Objekt sein. Um die Datei im Code-Editor zu öffnen, führen Sie den folgenden Befehl im Terminal aus:

code /home/labex/project/validate.py

Sobald die Datei geöffnet ist, scrollen Sie bis ans Ende und fügen Sie den folgenden Code hinzu:

class ValidatedFunction:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('Calling', self.func)
        result = self.func(*args, **kwargs)
        return result

Lassen Sie uns analysieren, was dieser Code tut. Die Klasse ValidatedFunction hat eine __init__-Methode, die der Konstruktor ist. Wenn Sie eine Instanz dieser Klasse erstellen, übergeben Sie ihr eine Funktion. Diese Funktion wird dann als Attribut der Instanz mit dem Namen self.func gespeichert.

Die __call__-Methode ist der Schlüsselteil, der diese Klasse aufrufbar macht. Wenn Sie eine Instanz der Klasse ValidatedFunction aufrufen, wird diese __call__-Methode ausgeführt. Hier ist, was sie Schritt für Schritt macht:

  1. Sie gibt eine Nachricht aus, die Ihnen mitteilt, welche Funktion aufgerufen wird. Dies ist nützlich für das Debugging und das Verständnis, was passiert.
  2. Sie ruft die Funktion auf, die in self.func gespeichert ist, mit den Argumenten, die Sie beim Aufruf der Instanz übergeben haben. Die *args und **kwargs ermöglichen es Ihnen, eine beliebige Anzahl von Positions- und Schlüsselwortargumenten zu übergeben.
  3. Sie gibt das Ergebnis des Funktionsaufrufs zurück.

Jetzt testen wir diese ValidatedFunction-Klasse. Wir werden eine neue Datei namens test_callable.py erstellen, um unseren Testcode zu schreiben. Um diese neue Datei im Code-Editor zu öffnen, führen Sie den folgenden Befehl aus:

code /home/labex/project/test_callable.py

Fügen Sie den folgenden Code zur Datei test_callable.py hinzu:

from validate import ValidatedFunction

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

## Wrap the add function with ValidatedFunction
validated_add = ValidatedFunction(add)

## Call the wrapped function
result = validated_add(2, 3)
print(f"Result: {result}")

## Try another call
result = validated_add(10, 20)
print(f"Result: {result}")

In diesem Code importieren wir zunächst die Klasse ValidatedFunction aus der Datei validate.py. Dann definieren wir eine einfache Funktion namens add, die zwei Zahlen nimmt und ihre Summe zurückgibt.

Wir erstellen eine Instanz der Klasse ValidatedFunction und übergeben ihr die Funktion add. Dies "umhüllt" die Funktion add in der Instanz der ValidatedFunction.

Wir rufen dann die umhüllte Funktion zweimal auf, einmal mit den Argumenten 2 und 3 und dann mit 10 und 20. Jedes Mal, wenn wir die umhüllte Funktion aufrufen, wird die __call__-Methode der Klasse ValidatedFunction aufgerufen, die wiederum die ursprüngliche Funktion add aufruft.

Um den Testcode auszuführen, führen Sie den folgenden Befehl im Terminal aus:

python3 /home/labex/project/test_callable.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Calling <function add at 0x7f2d1c3a9940>
Result: 5
Calling <function add at 0x7f2d1c3a9940>
Result: 30

Diese Ausgabe zeigt, dass unser aufrufbares Objekt wie erwartet funktioniert. Wenn wir validated_add(2, 3) aufrufen, rufen wir tatsächlich die __call__-Methode der Klasse ValidatedFunction auf, die dann die ursprüngliche Funktion add aufruft.

Zurzeit gibt unsere ValidatedFunction-Klasse nur eine Nachricht aus und leitet den Aufruf an die ursprüngliche Funktion weiter. Im nächsten Schritt werden wir diese Klasse verbessern, um eine Typüberprüfung basierend auf den Funktionsannotationen durchzuführen.

✨ Lösung prüfen und üben

Implementierung der Typüberprüfung mit Funktionsannotationen

In Python können Sie Typannotationen für Funktionsparameter hinzufügen. Diese Annotationen dienen dazu, die erwarteten Datentypen der Parameter und den Rückgabewert einer Funktion anzugeben. Standardmäßig erzwingen sie die Typen nicht zur Laufzeit, aber sie können für Validierungszwecke verwendet werden.

Schauen wir uns ein Beispiel an:

def add(x: int, y: int) -> int:
    return x + y

In diesem Code sagen x: int und y: int, dass die Parameter x und y Ganzzahlen sein sollten. Das -> int am Ende gibt an, dass die Funktion add eine Ganzzahl zurückgibt. Diese Typannotationen werden im __annotations__-Attribut der Funktion gespeichert, einem Wörterbuch, das die Parameternamen auf ihre annotierten Typen abbildet.

Jetzt werden wir unsere ValidatedFunction-Klasse erweitern, um diese Typannotationen für die Validierung zu nutzen. Dazu müssen wir das inspect-Modul von Python verwenden. Dieses Modul bietet nützliche Funktionen, um Informationen über lebende Objekte wie Module, Klassen, Methoden, Funktionen usw. zu erhalten. In unserem Fall werden wir es nutzen, um die Funktionsargumente mit ihren entsprechenden Parameternamen zu verknüpfen.

Zunächst müssen wir die ValidatedFunction-Klasse in der Datei validate.py ändern. Sie können diese Datei mit dem folgenden Befehl öffnen:

code /home/labex/project/validate.py

Ersetzen Sie die vorhandene ValidatedFunction-Klasse durch die folgende verbesserte Version:

import inspect

class ValidatedFunction:
    def __init__(self, func):
        self.func = func
        self.signature = inspect.signature(func)

    def __call__(self, *args, **kwargs):
        ## Bind the arguments to the function parameters
        bound = self.signature.bind(*args, **kwargs)

        ## Validate each argument against its annotation
        for name, val in bound.arguments.items():
            if name in self.func.__annotations__:
                ## Get the validator class from the annotation
                validator = self.func.__annotations__[name]
                ## Apply the validation
                validator.check(val)

        ## Call the function with the validated arguments
        return self.func(*args, **kwargs)

Hier ist, was diese verbesserte Version tut:

  1. Sie verwendet inspect.signature(), um Informationen über die Parameter der Funktion zu erhalten, wie ihre Namen, Standardwerte und annotierten Typen.
  2. Die bind()-Methode der Signatur wird verwendet, um die übergebenen Argumente mit ihren entsprechenden Parameternamen zu verknüpfen. Dies hilft uns, jedes Argument mit seinem richtigen Parameter in der Funktion zu assoziieren.
  3. Sie überprüft jedes Argument anhand seiner Typannotation (falls vorhanden). Wenn eine Annotation gefunden wird, ruft sie die Validator-Klasse aus der Annotation ab und wendet die Validierung mit der check()-Methode an.
  4. Schließlich ruft sie die ursprüngliche Funktion mit den validierten Argumenten auf.

Jetzt testen wir diese verbesserte ValidatedFunction-Klasse mit einigen Funktionen, die unsere Validator-Klassen in ihren Typannotationen verwenden. Öffnen Sie die Datei test_validation.py mit dem folgenden Befehl:

code /home/labex/project/test_validation.py

Fügen Sie den folgenden Code zur Datei hinzu:

from validate import ValidatedFunction, Integer, String

def greet(name: String, times: Integer):
    return name * times

## Wrap the greet function with ValidatedFunction
validated_greet = ValidatedFunction(greet)

## Valid call
try:
    result = validated_greet("Hello ", 3)
    print(f"Valid call result: {result}")
except TypeError as e:
    print(f"Unexpected error: {e}")

## Invalid call - wrong type for 'name'
try:
    result = validated_greet(123, 3)
    print(f"Invalid call unexpectedly succeeded: {result}")
except TypeError as e:
    print(f"Expected error for name: {e}")

## Invalid call - wrong type for 'times'
try:
    result = validated_greet("Hello ", "3")
    print(f"Invalid call unexpectedly succeeded: {result}")
except TypeError as e:
    print(f"Expected error for times: {e}")

In diesem Code definieren wir eine greet-Funktion mit den Typannotationen name: String und times: Integer. Dies bedeutet, dass der Parameter name mit der String-Klasse validiert werden sollte und der Parameter times mit der Integer-Klasse. Wir umhüllen dann die greet-Funktion mit unserer ValidatedFunction-Klasse, um die Typüberprüfung zu ermöglichen.

Wir führen drei Testfälle durch: einen gültigen Aufruf, einen ungültigen Aufruf mit dem falschen Typ für name und einen ungültigen Aufruf mit dem falschen Typ für times. Jeder Aufruf ist in einem try-except-Block eingeschlossen, um alle TypeError-Ausnahmen abzufangen, die während der Validierung auftreten können.

Um die Testdatei auszuführen, verwenden Sie den folgenden Befehl:

python3 /home/labex/project/test_validation.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Valid call result: Hello Hello Hello
Expected error for name: Expected <class 'str'>
Expected error for times: Expected <class 'int'>

Diese Ausgabe zeigt, dass unser aufrufbares ValidatedFunction-Objekt jetzt die Typüberprüfung basierend auf den Funktionsannotationen durchführt. Wenn wir Argumente des falschen Typs übergeben, erkennen die Validator-Klassen den Fehler und werfen einen TypeError. Auf diese Weise können wir sicherstellen, dass die Funktionen mit den richtigen Datentypen aufgerufen werden, was hilft, Fehler zu vermeiden und unseren Code robuster zu machen.

✨ Lösung prüfen und üben

Herausforderung: Verwenden eines aufrufbaren Objekts als Methode

In Python gibt es eine besondere Herausforderung, wenn Sie ein aufrufbares Objekt als Methode innerhalb einer Klasse verwenden. Ein aufrufbares Objekt ist etwas, das Sie wie eine Funktion "aufrufen" können, wie beispielsweise eine Funktion selbst oder ein Objekt mit einer __call__-Methode. Wenn es als Klassenmethode verwendet wird, funktioniert es nicht immer wie erwartet, weil Python die Instanz (self) als erstes Argument übergibt.

Lassen Sie uns dieses Problem untersuchen, indem wir eine Stock-Klasse erstellen. Diese Klasse wird eine Aktie repräsentieren, mit Attributen wie Name, Anzahl der Anteile und Preis. Wir werden auch einen Validator verwenden, um sicherzustellen, dass die Daten, mit denen wir arbeiten, korrekt sind.

Zunächst öffnen Sie die Datei stock.py, um unsere Stock-Klasse zu schreiben. Sie können den folgenden Befehl verwenden, um die Datei in einem Editor zu öffnen:

code /home/labex/project/stock.py

Fügen Sie jetzt den folgenden Code zur Datei stock.py hinzu. Dieser Code definiert die Stock-Klasse mit einer __init__-Methode, um die Attribute der Aktie zu initialisieren, einer cost-Eigenschaft, um die Gesamtkosten zu berechnen, und einer sell-Methode, um die Anzahl der Anteile zu reduzieren. Wir werden auch versuchen, die ValidatedFunction zu verwenden, um die Eingabe für die sell-Methode zu validieren.

from validate import ValidatedFunction, Integer

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

    def sell(self, nshares: Integer):
        self.shares -= nshares

    ## Try to use ValidatedFunction
    sell = ValidatedFunction(sell)

Nachdem Sie die Stock-Klasse definiert haben, müssen Sie sie testen, um zu sehen, ob sie wie erwartet funktioniert. Erstellen Sie eine Testdatei namens test_stock.py und öffnen Sie sie mit dem folgenden Befehl:

code /home/labex/project/test_stock.py

Fügen Sie den folgenden Code zur Datei test_stock.py hinzu. Dieser Code erstellt eine Instanz der Stock-Klasse, gibt die anfängliche Anzahl der Anteile und die Kosten aus, versucht, einige Anteile zu verkaufen, und gibt dann die aktualisierte Anzahl der Anteile und die Kosten aus.

from stock import Stock

try:
    ## Create a stock
    s = Stock('GOOG', 100, 490.1)

    ## Get the initial cost
    print(f"Initial shares: {s.shares}")
    print(f"Initial cost: ${s.cost}")

    ## Try to sell some shares
    s.sell(10)

    ## Check the updated cost
    print(f"After selling, shares: {s.shares}")
    print(f"After selling, cost: ${s.cost}")

except Exception as e:
    print(f"Error: {e}")

Jetzt führen Sie die Testdatei mit dem folgenden Befehl aus:

python3 /home/labex/project/test_stock.py

Sie werden wahrscheinlich einen Fehler ähnlich dem folgenden erhalten:

Error: missing a required argument: 'nshares'

Dieser Fehler tritt auf, weil Python, wenn es eine Methode wie s.sell(10) aufruft, tatsächlich Stock.sell(s, 10) im Hintergrund aufruft. Der self-Parameter repräsentiert die Instanz der Klasse und wird automatisch als erstes Argument übergeben. Allerdings behandelt unsere ValidatedFunction diesen self-Parameter nicht richtig, weil sie nicht weiß, dass sie als Methode verwendet wird.

Das Problem verstehen

Wenn Sie eine Methode innerhalb einer Klasse definieren und sie dann durch eine ValidatedFunction ersetzen, verpacken Sie im Grunde die ursprüngliche Methode. Das Problem ist, dass die verpackte Methode den self-Parameter nicht automatisch richtig behandelt. Sie erwartet die Argumente in einer Weise, die nicht berücksichtigt, dass die Instanz als erstes Argument übergeben wird.

Das Problem beheben

Um dieses Problem zu beheben, müssen wir die Art und Weise ändern, wie wir Methoden behandeln. Wir werden eine neue Klasse namens ValidatedMethod erstellen, die Methodenaufrufe richtig behandeln kann. Fügen Sie den folgenden Code ans Ende der Datei validate.py hinzu:

import inspect

class ValidatedMethod:
    def __init__(self, func):
        self.func = func
        self.signature = inspect.signature(func)

    def __get__(self, instance, owner):
        ## This method is called when the attribute is accessed as a method
        if instance is None:
            return self

        ## Return a callable that binds 'self' to the instance
        def method_wrapper(*args, **kwargs):
            return self(instance, *args, **kwargs)

        return method_wrapper

    def __call__(self, instance, *args, **kwargs):
        ## Bind the arguments to the function parameters
        bound = self.signature.bind(instance, *args, **kwargs)

        ## Validate each argument against its annotation
        for name, val in bound.arguments.items():
            if name in self.func.__annotations__:
                ## Get the validator class from the annotation
                validator = self.func.__annotations__[name]
                ## Apply the validation
                validator.check(val)

        ## Call the function with the validated arguments
        return self.func(instance, *args, **kwargs)

Jetzt müssen wir die Stock-Klasse ändern, um ValidatedMethod anstelle von ValidatedFunction zu verwenden. Öffnen Sie die Datei stock.py erneut:

code /home/labex/project/stock.py

Aktualisieren Sie die Stock-Klasse wie folgt:

from validate import ValidatedMethod, Integer

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

    @ValidatedMethod
    def sell(self, nshares: Integer):
        self.shares -= nshares

Die ValidatedMethod-Klasse ist ein Descriptor, ein spezieller Objekttyp in Python, der ändern kann, wie Attribute zugegriffen werden. Die __get__-Methode wird aufgerufen, wenn das Attribut als Methode zugegriffen wird. Sie gibt ein aufrufbares Objekt zurück, das die Instanz korrekt als erstes Argument übergibt.

Führen Sie die Testdatei erneut mit dem folgenden Befehl aus:

python3 /home/labex/project/test_stock.py

Jetzt sollten Sie eine Ausgabe ähnlich der folgenden sehen:

Initial shares: 100
Initial cost: $49010.0
After selling, shares: 90
After selling, cost: $44109.0

Diese Herausforderung hat Ihnen einen wichtigen Aspekt von aufrufbaren Objekten gezeigt. Wenn Sie sie als Methoden in einer Klasse verwenden, erfordern sie eine besondere Behandlung. Indem Sie das Descriptor-Protokoll mit der __get__-Methode implementieren, können Sie aufrufbare Objekte erstellen, die sowohl als eigenständige Funktionen als auch als Methoden richtig funktionieren.

Zusammenfassung

In diesem Lab haben Sie gelernt, wie man in Python geeignete aufrufbare Objekte erstellt. Zunächst haben Sie grundlegende Validator - Klassen für die Typüberprüfung untersucht und ein aufrufbares Objekt mit der __call__ - Methode erstellt. Dann haben Sie dieses Objekt erweitert, um eine Validierung basierend auf Funktionsannotationen durchzuführen, und die Herausforderung bewältigt, aufrufbare Objekte als Klassenmethoden zu verwenden.

Zu den behandelten Schlüsselkonzepten gehören aufrufbare Objekte und die __call__ - Methode, Funktionsannotationen für Typhinweise, die Verwendung des inspect - Moduls zur Untersuchung von Funktionssignaturen und das Descriptor - Protokoll mit der __get__ - Methode für Klassenmethoden. Mit diesen Techniken können Sie leistungsstarke Funktionsumhüllungen (function wrappers) für die Vor - und Nachverarbeitung von Funktionsaufrufen erstellen, was ein grundlegendes Muster für Dekorateure und andere fortgeschrittene Python - Funktionen ist.