Lernen Sie etwas über Deskriptoren (Descriptors)

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 über Descriptor (Beschreiber) in Python kennen, einem leistungsstarken Mechanismus zur Anpassung des Zugriffs auf Attribute in Objekten. Descriptor ermöglichen es Ihnen, zu definieren, wie Attribute zugegriffen, festgelegt und gelöscht werden, wodurch Sie die Objektverhalten steuern und die Implementierung von Validierungslogik ermöglichen.

Die Ziele dieses Labs umfassen das Verständnis des Descriptor-Protokolls, das Erstellen und Verwenden von benutzerdefinierten Descriptor, die Implementierung von Datenvalidierung mit Descriptor und die Optimierung der Descriptor-Implementierungen. Sie werden während des Labs mehrere Dateien erstellen, darunter descrip.py, stock.py und validate.py.

Dies ist ein Guided Lab, das schrittweise Anweisungen bietet, um Ihnen beim Lernen und Üben zu helfen. Befolgen Sie die Anweisungen sorgfältig, um jeden Schritt abzuschließen und praktische Erfahrungen zu sammeln. Historische Daten zeigen, dass dies ein Labor der Stufe Anfänger mit einer Abschlussquote von 91% ist. Es hat eine positive Bewertungsrate von 100% von den Lernenden erhalten.

Verständnis des Descriptor-Protokolls

In diesem Schritt werden wir lernen, wie Descriptor in Python funktionieren, indem wir eine einfache Stock-Klasse erstellen. Descriptor in Python sind ein leistungsstarkes Feature, das es Ihnen ermöglicht, anzupassen, wie Attribute zugegriffen, festgelegt und gelöscht werden. Das Descriptor-Protokoll besteht aus drei speziellen Methoden: __get__(), __set__() und __delete__(). Diese Methoden definieren, wie sich der Descriptor verhält, wenn auf ein Attribut zugegriffen wird, ihm ein Wert zugewiesen wird oder es gelöscht wird.

Zunächst müssen wir im Projektverzeichnis eine neue Datei namens stock.py erstellen. Diese Datei wird unsere Stock-Klasse enthalten. Hier ist der Code, den Sie in die stock.py-Datei einfügen sollten:

## stock.py

class Stock:
    __slots__ = ['_name', '_shares', '_price']

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._name = value

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected an integer')
        if value < 0:
            raise ValueError('Expected a positive value')
        self._shares = value

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError('Expected a number')
        if value < 0:
            raise ValueError('Expected a positive value')
        self._price = value

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

    def sell(self, amount):
        self.shares -= amount

    def __repr__(self):
        return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'

In dieser Stock-Klasse verwenden wir den property-Decorator, um Getter- und Setter-Methoden für die Attribute name, shares und price zu definieren. Diese Getter- und Setter-Methoden fungieren als Descriptor, was bedeutet, dass sie steuern, wie auf diese Attribute zugegriffen und sie festgelegt werden. Beispielsweise validieren die Setter-Methoden die Eingabewerte, um sicherzustellen, dass sie vom richtigen Typ sind und innerhalb eines akzeptablen Bereichs liegen.

Jetzt, da unsere stock.py-Datei fertig ist, öffnen wir eine Python-Shell, um mit der Stock-Klasse zu experimentieren und zu sehen, wie Descriptor in der Praxis funktionieren. Dazu öffnen Sie Ihr Terminal und führen Sie die folgenden Befehle aus:

cd ~/project
python3 -i stock.py

Die -i-Option im python3-Befehl teilt Python mit, eine interaktive Shell zu starten, nachdem die stock.py-Datei ausgeführt wurde. Auf diese Weise können wir direkt mit der Stock-Klasse interagieren, die wir gerade definiert haben.

In der Python-Shell erstellen wir ein Aktienobjekt und versuchen, auf seine Attribute zuzugreifen. So können Sie es tun:

s = Stock('GOOG', 100, 490.10)
s.name      ## Should return 'GOOG'
s.shares    ## Should return 100

Wenn Sie auf die Attribute name und shares des Objekts s zugreifen, verwendet Python tatsächlich die __get__-Methode des Descriptors im Hintergrund. Die property-Decoratoren in unserer Klasse werden mit Descriptor implementiert, was bedeutet, dass sie den Zugriff auf und die Zuweisung von Attributen auf kontrollierte Weise handhaben.

Schauen wir uns genauer das Klassenwörterbuch an, um die Descriptor-Objekte zu sehen. Das Klassenwörterbuch enthält alle Attribute und Methoden, die in der Klasse definiert sind. Sie können die Schlüssel des Klassenwörterbuchs mit dem folgenden Code anzeigen:

Stock.__dict__.keys()

Sie sollten eine Ausgabe ähnlich dieser sehen:

dict_keys(['__module__', '__slots__', '__init__', 'name', '_name', 'shares', '_shares', 'price', '_price', 'cost', 'sell', '__repr__', '__doc__'])

Die Schlüssel name, shares und price repräsentieren die Descriptor-Objekte, die von den property-Decoratoren erstellt wurden.

Jetzt untersuchen wir, wie Descriptor funktionieren, indem wir ihre Methoden manuell aufrufen. Wir verwenden den shares-Descriptor als Beispiel. So können Sie es tun:

## Get the descriptor object for 'shares'
q = Stock.__dict__['shares']

## Use the __get__ method to retrieve the value
q.__get__(s, Stock)  ## Should return 100

## Use the __set__ method to set a new value
q.__set__(s, 75)
s.shares  ## Should now be 75

## Try to set an invalid value
try:
    q.__set__(s, '75')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

Wenn Sie auf ein Attribut wie s.shares zugreifen, ruft Python die __get__-Methode des Descriptors auf, um den Wert abzurufen. Wenn Sie einen Wert wie s.shares = 75 zuweisen, ruft Python die __set__-Methode des Descriptors auf. Der Descriptor kann dann die Daten validieren und Fehler auslösen, wenn der Eingabewert ungültig ist.

Sobald Sie mit der Stock-Klasse und den Descriptorn experimentiert haben, können Sie die Python-Shell beenden, indem Sie den folgenden Befehl ausführen:

exit()

Erstellen von benutzerdefinierten Descriptor

In diesem Schritt werden wir unsere eigene Descriptor-Klasse erstellen. Aber zunächst verstehen wir, was ein Descriptor ist. Ein Descriptor ist ein Python-Objekt, das das Descriptor-Protokoll implementiert, das aus den Methoden __get__, __set__ und __delete__ besteht. Diese Methoden ermöglichen es dem Descriptor, zu verwalten, wie auf ein Attribut zugegriffen, es festgelegt und gelöscht wird. Indem wir unsere eigene Descriptor-Klasse erstellen, können wir besser verstehen, wie dieses Protokoll funktioniert.

Erstellen Sie im Projektverzeichnis eine neue Datei namens descrip.py. Diese Datei wird unsere benutzerdefinierte Descriptor-Klasse enthalten. Hier ist der Code:

## descrip.py

class Descriptor:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, cls):
        print(f'{self.name}:__get__')
        ## In a real descriptor, you would return a value here

    def __set__(self, instance, value):
        print(f'{self.name}:__set__ {value}')
        ## In a real descriptor, you would store the value here

    def __delete__(self, instance):
        print(f'{self.name}:__delete__')
        ## In a real descriptor, you would delete the value here

In der Descriptor-Klasse initialisiert die __init__-Methode den Descriptor mit einem Namen. Die __get__-Methode wird aufgerufen, wenn auf das Attribut zugegriffen wird, die __set__-Methode wird aufgerufen, wenn das Attribut festgelegt wird, und die __delete__-Methode wird aufgerufen, wenn das Attribut gelöscht wird.

Jetzt erstellen wir eine Testdatei, um mit unserem benutzerdefinierten Descriptor zu experimentieren. Dies hilft uns zu verstehen, wie sich der Descriptor in verschiedenen Szenarien verhält. Erstellen Sie eine Datei namens test_descrip.py mit dem folgenden Code:

## test_descrip.py

from descrip import Descriptor

class Foo:
    a = Descriptor('a')
    b = Descriptor('b')
    c = Descriptor('c')

## Create an instance and try accessing the attributes
if __name__ == '__main__':
    f = Foo()
    print("Accessing attribute f.a:")
    f.a

    print("\nAccessing attribute f.b:")
    f.b

    print("\nSetting attribute f.a = 23:")
    f.a = 23

    print("\nDeleting attribute f.a:")
    del f.a

In der test_descrip.py-Datei importieren wir die Descriptor-Klasse aus descrip.py. Dann erstellen wir eine Klasse Foo mit drei Attributen a, b und c, die jeweils von einem Descriptor verwaltet werden. Wir erstellen eine Instanz von Foo und führen Operationen wie Zugriff, Festlegung und Löschung von Attributen aus, um zu sehen, wie die Descriptor-Methoden aufgerufen werden.

Jetzt führen wir diese Testdatei aus, um die Descriptor in Aktion zu sehen. Öffnen Sie Ihr Terminal, navigieren Sie zum Projektverzeichnis und führen Sie die Testdatei mit den folgenden Befehlen aus:

cd ~/project
python3 test_descrip.py

Sie sollten eine Ausgabe wie diese sehen:

Accessing attribute f.a:
a:__get__

Accessing attribute f.b:
b:__get__

Setting attribute f.a = 23:
a:__set__ 23

Deleting attribute f.a:
a:__delete__

Wie Sie sehen können, wird jedes Mal, wenn Sie auf ein Attribut zugreifen, es festlegen oder löschen, das von einem Descriptor verwaltet wird, die entsprechende magische Methode (__get__, __set__ oder __delete__) aufgerufen.

Lassen Sie uns auch unseren Descriptor interaktiv untersuchen. Dies ermöglicht es uns, den Descriptor in Echtzeit zu testen und die Ergebnisse sofort zu sehen. Öffnen Sie Ihr Terminal, navigieren Sie zum Projektverzeichnis und starten Sie eine interaktive Python-Sitzung mit der descrip.py-Datei:

cd ~/project
python3 -i descrip.py

Geben Sie jetzt diese Befehle in der interaktiven Python-Sitzung ein, um zu sehen, wie das Descriptor-Protokoll funktioniert:

class Foo:
    a = Descriptor('a')
    b = Descriptor('b')
    c = Descriptor('c')

f = Foo()
f.a         ## Should call __get__
f.b         ## Should call __get__
f.a = 23    ## Should call __set__
del f.a     ## Should call __delete__
exit()

Der wichtigste Aspekt hier ist, dass Descriptor eine Möglichkeit bieten, den Zugriff auf Attribute abzufangen und anzupassen. Dies macht sie leistungsstark für die Implementierung von Datenvalidierung, berechneten Attributen und anderen fortgeschrittenen Verhaltensweisen. Mit Descriptor haben Sie mehr Kontrolle darüber, wie auf die Klassenattribute zugegriffen, sie festgelegt und gelöscht werden.

✨ Lösung prüfen und üben

Implementierung von Validatoren mit Descriptor

In diesem Schritt werden wir ein Validierungssystem mit Descriptor erstellen. Zunächst verstehen wir, was Descriptor sind und warum wir sie verwenden. Descriptor sind Python-Objekte, die das Descriptor-Protokoll implementieren, das die Methoden __get__, __set__ oder __delete__ umfasst. Sie ermöglichen es Ihnen, anzupassen, wie auf ein Attribut eines Objekts zugegriffen, es festgelegt oder gelöscht wird. In unserem Fall verwenden wir Descriptor, um ein Validierungssystem zu erstellen, das die Datenintegrität gewährleistet. Das bedeutet, dass die in unseren Objekten gespeicherten Daten immer bestimmten Kriterien entsprechen, wie z. B. einem bestimmten Datentyp oder einem positiven Wert.

Jetzt beginnen wir mit der Erstellung unseres Validierungssystems. Wir erstellen im Projektverzeichnis eine neue Datei namens validate.py. Diese Datei wird die Klassen enthalten, die unsere Validatoren implementieren.

## validate.py

class Validator:
    def __init__(self, name):
        self.name = name

    @classmethod
    def check(cls, value):
        return value

    def __set__(self, instance, value):
        instance.__dict__[self.name] = self.check(value)


class String(Validator):
    expected_type = str

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        return value


class PositiveInteger(Validator):
    expected_type = int

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        if value < 0:
            raise ValueError('Expected a positive value')
        return value


class PositiveFloat(Validator):
    expected_type = float

    @classmethod
    def check(cls, value):
        if not isinstance(value, (int, float)):
            raise TypeError('Expected a number')
        if value < 0:
            raise ValueError('Expected a positive value')
        return float(value)

In der validate.py-Datei definieren wir zunächst eine Basisklasse namens Validator. Diese Klasse hat eine __init__-Methode, die einen name-Parameter akzeptiert, der verwendet wird, um das zu validierende Attribut zu identifizieren. Die check-Methode ist eine Klassenmethode, die einfach den ihr übergebenen Wert zurückgibt. Die __set__-Methode ist eine Descriptor-Methode, die aufgerufen wird, wenn ein Attribut an einem Objekt festgelegt wird. Sie ruft die check-Methode auf, um den Wert zu validieren, und speichert dann den validierten Wert im Wörterbuch des Objekts.

Anschließend definieren wir drei Unterklassen von Validator: String, PositiveInteger und PositiveFloat. Jede dieser Unterklassen überschreibt die check-Methode, um spezifische Validierungsüberprüfungen durchzuführen. Die String-Klasse überprüft, ob der Wert eine Zeichenkette ist, die PositiveInteger-Klasse überprüft, ob der Wert eine positive Ganzzahl ist, und die PositiveFloat-Klasse überprüft, ob der Wert eine positive Zahl (entweder eine Ganzzahl oder eine Fließkommazahl) ist.

Jetzt, da wir unsere Validatoren definiert haben, ändern wir unsere Stock-Klasse, um diese Validatoren zu verwenden. Wir erstellen eine neue Datei namens stock_with_validators.py und importieren die Validatoren aus der validate.py-Datei.

## stock_with_validators.py

from validate import String, PositiveInteger, PositiveFloat

class Stock:
    name = String('name')
    shares = PositiveInteger('shares')
    price = PositiveFloat('price')

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

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

    def sell(self, amount):
        self.shares -= amount

    def __repr__(self):
        return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'

In der stock_with_validators.py-Datei definieren wir die Stock-Klasse und verwenden die Validatoren als Klassenattribute. Das bedeutet, dass jedes Mal, wenn ein Attribut an einem Stock-Objekt festgelegt wird, die __set__-Methode des entsprechenden Validators aufgerufen wird, um den Wert zu validieren. Die __init__-Methode initialisiert die Attribute des Stock-Objekts, und die cost, sell und __repr__-Methoden bieten zusätzliche Funktionalität.

Jetzt testen wir unsere auf Validatoren basierende Stock-Klasse. Wir öffnen ein Terminal, navigieren zum Projektverzeichnis und führen die stock_with_validators.py-Datei im interaktiven Modus aus.

cd ~/project
python3 -i stock_with_validators.py

Sobald der Python-Interpreter läuft, können wir einige Befehle ausprobieren, um das Validierungssystem zu testen.

## Create a stock with valid values
s = Stock('GOOG', 100, 490.10)
print(s.name)    ## Should return 'GOOG'
print(s.shares)  ## Should return 100
print(s.price)   ## Should return 490.1

## Try changing to valid values
s.shares = 75
print(s.shares)  ## Should return 75

## Try setting invalid values
try:
    s.shares = '75'  ## Should raise TypeError
except TypeError as e:
    print(f"Error setting shares to string: {e}")

try:
    s.shares = -50  ## Should raise ValueError
except ValueError as e:
    print(f"Error setting negative shares: {e}")

exit()

Im Testcode erstellen wir zunächst ein Stock-Objekt mit gültigen Werten und geben seine Attribute aus, um zu überprüfen, ob sie korrekt festgelegt wurden. Dann versuchen wir, das shares-Attribut auf einen gültigen Wert zu ändern und geben es erneut aus, um die Änderung zu bestätigen. Schließlich versuchen wir, das shares-Attribut auf einen ungültigen Wert (eine Zeichenkette und eine negative Zahl) zu setzen und fangen die von den Validatoren ausgelösten Ausnahmen ab.

Beachten Sie, wie unser Code jetzt viel sauberer ist. Die Stock-Klasse muss nicht mehr alle diese Eigenschaftsmethoden implementieren - die Validatoren übernehmen alle Typüberprüfungen und Einschränkungen.

Descriptor haben es uns ermöglicht, ein wiederverwendbares Validierungssystem zu erstellen, das auf jedes Klassenattribut angewendet werden kann. Dies ist ein leistungsstarkes Muster für die Aufrechterhaltung der Datenintegrität in Ihrer Anwendung.

✨ Lösung prüfen und üben

Verbesserung der Descriptor-Implementierung

In diesem Schritt werden wir unsere Descriptor-Implementierung verbessern. Sie haben vielleicht bemerkt, dass wir in einigen Fällen Namen redundant angegeben haben. Dies kann unseren Code etwas unübersichtlich und schwieriger zu warten machen. Um dieses Problem zu lösen, verwenden wir die __set_name__-Methode, eine nützliche Funktion, die in Python 3.6 eingeführt wurde.

Die __set_name__-Methode wird automatisch aufgerufen, wenn die Klasse definiert wird. Ihre Hauptaufgabe ist es, den Namen des Descriptors für uns festzulegen, sodass wir dies nicht jedes Mal manuell tun müssen. Dies macht unseren Code sauberer und effizienter.

Jetzt aktualisieren wir Ihre validate.py-Datei, um die __set_name__-Methode einzubeziehen. So wird der aktualisierte Code aussehen:

## validate.py

class Validator:
    def __init__(self, name=None):
        self.name = name

    def __set_name__(self, cls, name):
        ## This gets called when the class is defined
        ## It automatically sets the name of the descriptor
        if self.name is None:
            self.name = name

    @classmethod
    def check(cls, value):
        return value

    def __set__(self, instance, value):
        instance.__dict__[self.name] = self.check(value)


class String(Validator):
    expected_type = str

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        return value


class PositiveInteger(Validator):
    expected_type = int

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        if value < 0:
            raise ValueError('Expected a positive value')
        return value


class PositiveFloat(Validator):
    expected_type = float

    @classmethod
    def check(cls, value):
        if not isinstance(value, (int, float)):
            raise TypeError('Expected a number')
        if value < 0:
            raise ValueError('Expected a positive value')
        return float(value)

Im obigen Code überprüft die __set_name__-Methode in der Validator-Klasse, ob das name-Attribut None ist. Wenn dies der Fall ist, setzt es das name auf den tatsächlichen Attributnamen, der in der Klassendefinition verwendet wird. Auf diese Weise müssen wir den Namen nicht explizit angeben, wenn wir Instanzen der Descriptor-Klassen erstellen.

Jetzt, da wir die validate.py-Datei aktualisiert haben, können wir eine verbesserte Version unserer Stock-Klasse erstellen. Diese neue Version erfordert es uns nicht mehr, die Namen redundant anzugeben. Hier ist der Code für die verbesserte Stock-Klasse:

## improved_stock.py

from validate import String, PositiveInteger, PositiveFloat

class Stock:
    name = String()  ## No need to specify 'name' anymore
    shares = PositiveInteger()
    price = PositiveFloat()

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

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

    def sell(self, amount):
        self.shares -= amount

    def __repr__(self):
        return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'

In dieser Stock-Klasse erstellen wir einfach Instanzen der String, PositiveInteger und PositiveFloat Descriptor-Klassen, ohne die Namen anzugeben. Die __set_name__-Methode in der Validator-Klasse kümmert sich automatisch um das Festlegen der Namen.

Lassen Sie uns unsere verbesserte Stock-Klasse testen. Öffnen Sie zunächst Ihr Terminal und navigieren Sie zum Projektverzeichnis. Führen Sie dann die improved_stock.py-Datei im interaktiven Modus aus. Hier sind die Befehle dazu:

cd ~/project
python3 -i improved_stock.py

Sobald Sie in der interaktiven Python-Sitzung sind, können Sie die folgenden Befehle ausprobieren, um die Funktionalität der Stock-Klasse zu testen:

s = Stock('GOOG', 100, 490.10)
print(s.name)    ## Should return 'GOOG'
print(s.shares)  ## Should return 100
print(s.price)   ## Should return 490.1

## Try changing values
s.shares = 75
print(s.shares)  ## Should return 75

## Try invalid values
try:
    s.shares = '75'  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

try:
    s.price = -10.5  ## Should raise ValueError
except ValueError as e:
    print(f"Error: {e}")

exit()

Diese Befehle erstellen eine Instanz der Stock-Klasse, geben ihre Attribute aus, ändern den Wert eines Attributs und versuchen dann, ungültige Werte zu setzen, um zu sehen, ob die entsprechenden Fehler ausgelöst werden.

Die __set_name__-Methode legt automatisch den Namen des Descriptors fest, wenn die Klasse definiert wird. Dies macht Ihren Code sauberer und weniger redundant, da Sie den Attributnamen nicht mehr zweimal angeben müssen.

Diese Verbesserung zeigt, wie sich das Descriptor-Protokoll von Python weiterentwickelt und es einfacher macht, sauberen und wartbaren Code zu schreiben.

✨ Lösung prüfen und üben

Zusammenfassung

In diesem Lab haben Sie sich mit Python-Deskriptoren (Descriptor) vertraut gemacht, einer leistungsstarken Funktion, die es ermöglicht, den Zugriff auf Attribute in Klassen anzupassen. Sie haben das Deskriptor-Protokoll (Descriptor Protocol) untersucht, einschließlich der Methoden __get__, __set__ und __delete__. Sie haben auch eine grundlegende Deskriptor-Klasse (Descriptor Class) erstellt, um den Zugriff auf Attribute abzufangen, und Deskriptoren verwendet, um ein Validierungssystem für die Datenintegrität zu implementieren.

Darüber hinaus haben Sie Ihre Deskriptoren mit der __set_name__-Methode verbessert, um Redundanzen zu reduzieren. Deskriptoren werden in Python-Bibliotheken und -Frameworks wie Django und SQLAlchemy weit verbreitet eingesetzt. Wenn Sie sie verstehen, erhalten Sie tiefere Einblicke in Python und können eleganteren und wartbareren Code schreiben.