Anpassen des Attributzugriffs

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 einen grundlegenden Aspekt der objektorientierten Programmierung in Python kennenlernen: den Zugriff auf Attribute. Python ermöglicht es Entwicklern, über spezielle Methoden zu definieren, wie Attribute in Klassen zugegriffen, gesetzt und verwaltet werden. Dies bietet leistungsstarke Möglichkeiten, das Verhalten von Objekten zu steuern.

Darüber hinaus werden Sie lernen, wie Sie den Zugriff auf Attribute in Python-Klassen anpassen können, den Unterschied zwischen Delegation und Vererbung verstehen und die Implementierung einer benutzerdefinierten Attributverwaltung in Python-Objekten üben.

Verständnis von __setattr__ für die Attributsteuerung

In Python gibt es spezielle Methoden, die es Ihnen ermöglichen, anzupassen, wie auf Attribute eines Objekts zugegriffen und diese modifiziert werden. Eine solche wichtige Methode ist __setattr__(). Diese Methode wird jedes Mal aufgerufen, wenn Sie versuchen, einem Attribut eines Objekts einen Wert zuzuweisen. Sie ermöglicht es Ihnen, den Prozess der Attributzuweisung fein abzustimmen.

Was ist __setattr__?

Die Methode __setattr__(self, name, value) fungiert als Interceptor für alle Attributzuweisungen. Wenn Sie eine einfache Zuweisungsanweisung wie obj.attr = value schreiben, weist Python den Wert nicht einfach direkt zu. Stattdessen ruft es intern obj.__setattr__("attr", value) auf. Dieser Mechanismus gibt Ihnen die Möglichkeit zu entscheiden, was während der Attributzuweisung passieren soll.

Schauen wir uns nun ein praktisches Beispiel an, wie wir __setattr__ verwenden können, um zu beschränken, welche Attribute in einer Klasse gesetzt werden können.

Schritt 1: Erstellen einer neuen Datei

Öffnen Sie zunächst eine neue Datei in der WebIDE. Sie können dies tun, indem Sie auf das Menü "File" klicken und dann "New File" auswählen. Benennen Sie diese Datei restricted_stock.py und speichern Sie sie im Verzeichnis /home/labex/project. In dieser Datei wird die Klassendefinition enthalten sein, in der wir __setattr__ verwenden, um die Attributzuweisung zu steuern.

Schritt 2: Hinzufügen von Code zu restricted_stock.py

Fügen Sie den folgenden Code zur Datei restricted_stock.py hinzu. Dieser Code definiert eine Klasse RestrictedStock.

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

    def __setattr__(self, name, value):
        ## Only allow specific attributes
        if name not in {'name', 'shares', 'price'}:
            raise AttributeError(f'Cannot set attribute {name}')

        ## If attribute is allowed, set it using the parent method
        super().__setattr__(name, value)

In der __init__-Methode initialisieren wir das Objekt mit den Attributen name, shares und price. Die __setattr__-Methode prüft, ob der Name des zuzuweisenden Attributs in der Menge der erlaubten Attribute (name, shares, price) enthalten ist. Wenn dies nicht der Fall ist, wird ein AttributeError ausgelöst. Wenn das Attribut erlaubt ist, wird die __setattr__-Methode der Basisklasse verwendet, um das Attribut tatsächlich zu setzen.

Schritt 3: Erstellen einer Testdatei

Erstellen Sie eine neue Datei namens test_restricted.py und fügen Sie den folgenden Code hinzu. Dieser Code wird die Funktionalität der Klasse RestrictedStock testen.

from restricted_stock import RestrictedStock

## Create a new stock
stock = RestrictedStock('GOOG', 100, 490.1)

## Test accessing existing attributes
print(f"Name: {stock.name}")
print(f"Shares: {stock.shares}")
print(f"Price: {stock.price}")

## Test modifying an existing attribute
print("\nChanging shares to 75...")
stock.shares = 75
print(f"New shares value: {stock.shares}")

## Test setting an invalid attribute
try:
    print("\nTrying to set an invalid attribute 'share'...")
    stock.share = 50
except AttributeError as e:
    print(f"Error: {e}")

In diesem Code importieren wir zunächst die Klasse RestrictedStock. Dann erstellen wir eine Instanz der Klasse. Wir testen den Zugriff auf vorhandene Attribute, die Modifikation eines vorhandenen Attributs und versuchen schließlich, ein ungültiges Attribut zu setzen, um zu überprüfen, ob die __setattr__-Methode wie erwartet funktioniert.

Schritt 4: Ausführen der Testdatei

Öffnen Sie ein Terminal in der WebIDE und führen Sie die folgenden Befehle aus, um die Datei test_restricted.py auszuführen:

cd /home/labex/project
python3 test_restricted.py

Nachdem Sie diese Befehle ausgeführt haben, sollten Sie eine Ausgabe ähnlich der folgenden sehen:

Name: GOOG
Shares: 100
Price: 490.1

Changing shares to 75...
New shares value: 75

Trying to set an invalid attribute 'share'...
Error: Cannot set attribute share

Wie es funktioniert

Die __setattr__-Methode in unserer Klasse RestrictedStock funktioniert in folgenden Schritten:

  1. Sie prüft zunächst, ob der Attributname in der Menge der erlaubten Attribute (name, shares, price) enthalten ist.
  2. Wenn der Attributname nicht in der erlaubten Menge enthalten ist, wird ein AttributeError ausgelöst. Dies verhindert die Zuweisung von unerwünschten Attributen.
  3. Wenn das Attribut erlaubt ist, wird super().__setattr__() verwendet, um das Attribut tatsächlich zu setzen. Dies stellt sicher, dass der normale Prozess der Attributzuweisung für die erlaubten Attribute stattfindet.

Diese Methode ist flexibler als die Verwendung von __slots__, wie wir es in früheren Beispielen gesehen haben. Während __slots__ die Speichernutzung optimieren und Attribute einschränken kann, hat es Einschränkungen bei der Arbeit mit Vererbung und kann mit anderen Python-Funktionen in Konflikt geraten. Unser Ansatz mit __setattr__ gibt uns ähnliche Kontrolle über die Attributzuweisung ohne einige dieser Einschränkungen.

Erstellen von schreibgeschützten Objekten mit Proxies

In diesem Schritt werden wir Proxy-Klassen untersuchen, ein sehr nützliches Muster in Python. Proxy-Klassen ermöglichen es Ihnen, ein vorhandenes Objekt zu nehmen und sein Verhalten zu ändern, ohne seinen ursprünglichen Code zu verändern. Dies ist wie das Umwickeln eines Objekts in eine spezielle Hülle, um neue Funktionen hinzuzufügen oder Einschränkungen zu setzen.

Was ist ein Proxy?

Ein Proxy ist ein Objekt, das zwischen Ihnen und einem anderen Objekt steht. Es hat die gleichen Funktionen und Eigenschaften wie das ursprüngliche Objekt, kann aber zusätzliche Dinge tun. Beispielsweise kann es kontrollieren, wer auf das Objekt zugreifen kann, Aktionen protokollieren (Logging) oder andere nützliche Funktionen hinzufügen.

Lassen Sie uns einen schreibgeschützten Proxy erstellen. Dieser Proxy wird es Ihnen verhindern, die Attribute eines Objekts zu ändern.

Schritt 1: Erstellen der schreibgeschützten Proxy-Klasse

Zunächst müssen wir eine Python-Datei erstellen, die unseren schreibgeschützten Proxy definiert.

  1. Navigieren Sie zum Verzeichnis /home/labex/project.
  2. Erstellen Sie in diesem Verzeichnis eine neue Datei namens readonly_proxy.py.
  3. Öffnen Sie die Datei readonly_proxy.py und fügen Sie den folgenden Code hinzu:
class ReadonlyProxy:
    def __init__(self, obj):
        ## Store the wrapped object directly in __dict__ to avoid triggering __setattr__
        self.__dict__['_obj'] = obj

    def __getattr__(self, name):
        ## Forward attribute access to the wrapped object
        return getattr(self._obj, name)

    def __setattr__(self, name, value):
        ## Block all attribute assignments
        raise AttributeError("Cannot modify a read-only object")

In diesem Code wird die Klasse ReadonlyProxy definiert. Die __init__-Methode speichert das Objekt, das wir umwickeln möchten. Wir verwenden self.__dict__, um es direkt zu speichern, um die __setattr__-Methode nicht aufzurufen. Die __getattr__-Methode wird verwendet, wenn wir versuchen, auf ein Attribut des Proxys zuzugreifen. Sie leitet einfach die Anfrage an das umwickelte Objekt weiter. Die __setattr__-Methode wird aufgerufen, wenn wir versuchen, ein Attribut zu ändern. Sie löst einen Fehler aus, um jegliche Änderungen zu verhindern.

Schritt 2: Erstellen einer Testdatei

Jetzt erstellen wir eine Testdatei, um zu sehen, wie unser schreibgeschützter Proxy funktioniert.

  1. Erstellen Sie in demselben Verzeichnis /home/labex/project eine neue Datei namens test_readonly.py.
  2. Fügen Sie der Datei test_readonly.py den folgenden Code hinzu:
from stock import Stock
from readonly_proxy import ReadonlyProxy

## Create a normal Stock object
stock = Stock('AAPL', 100, 150.75)
print("Original stock object:")
print(f"Name: {stock.name}")
print(f"Shares: {stock.shares}")
print(f"Price: {stock.price}")
print(f"Cost: {stock.cost}")

## Modify the original stock object
stock.shares = 200
print(f"\nAfter modification - Shares: {stock.shares}")
print(f"After modification - Cost: {stock.cost}")

## Create a read-only proxy around the stock
readonly_stock = ReadonlyProxy(stock)
print("\nRead-only proxy object:")
print(f"Name: {readonly_stock.name}")
print(f"Shares: {readonly_stock.shares}")
print(f"Price: {readonly_stock.price}")
print(f"Cost: {readonly_stock.cost}")

## Try to modify the read-only proxy
try:
    print("\nAttempting to modify the read-only proxy...")
    readonly_stock.shares = 300
except AttributeError as e:
    print(f"Error: {e}")

## Show that the original object is unchanged
print(f"\nOriginal stock shares are still: {stock.shares}")

In diesem Testcode erstellen wir zunächst ein normales Stock-Objekt und geben seine Informationen aus. Dann ändern wir eines seiner Attribute und geben die aktualisierten Informationen aus. Als Nächstes erstellen wir einen schreibgeschützten Proxy für das Stock-Objekt und geben seine Informationen aus. Schließlich versuchen wir, den schreibgeschützten Proxy zu ändern und erwarten einen Fehler.

Schritt 3: Ausführen des Testskripts

Nachdem wir die Proxy-Klasse und die Testdatei erstellt haben, müssen wir das Testskript ausführen, um die Ergebnisse zu sehen.

  1. Öffnen Sie ein Terminal und navigieren Sie mit dem folgenden Befehl zum Verzeichnis /home/labex/project:
cd /home/labex/project
  1. Führen Sie das Testskript mit dem folgenden Befehl aus:
python3 test_readonly.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Original stock object:
Name: AAPL
Shares: 100
Price: 150.75
Cost: 15075.0

After modification - Shares: 200
After modification - Cost: 30150.0

Read-only proxy object:
Name: AAPL
Shares: 200
Price: 150.75
Cost: 30150.0

Attempting to modify the read-only proxy...
Error: Cannot modify a read-only object

Original stock shares are still: 200

Wie der Proxy funktioniert

Die Klasse ReadonlyProxy verwendet zwei spezielle Methoden, um ihre schreibgeschützte Funktionalität zu erreichen:

  1. __getattr__(self, name): Diese Methode wird aufgerufen, wenn Python ein Attribut nicht auf die normale Weise finden kann. In unserer ReadonlyProxy-Klasse verwenden wir die getattr()-Funktion, um die Anfrage zum Zugriff auf das Attribut an das umwickelte Objekt weiterzuleiten. Wenn Sie also versuchen, auf ein Attribut des Proxys zuzugreifen, wird es tatsächlich das Attribut aus dem umwickelten Objekt abrufen.

  2. __setattr__(self, name, value): Diese Methode wird aufgerufen, wenn Sie versuchen, einem Attribut einen Wert zuzuweisen. In unserer Implementierung lösen wir einen AttributeError aus, um jegliche Änderungen an den Attributen des Proxys zu verhindern.

  3. In der __init__-Methode ändern wir direkt self.__dict__, um das umwickelte Objekt zu speichern. Dies ist wichtig, denn wenn wir die normale Weise zur Zuweisung des Objekts verwenden würden, würde die __setattr__-Methode aufgerufen werden, die einen Fehler auslösen würde.

Dieses Proxy-Muster ermöglicht es uns, eine schreibgeschützte Schicht um jedes vorhandene Objekt hinzuzufügen, ohne seine ursprüngliche Klasse zu ändern. Das Proxy-Objekt verhält sich wie das umwickelte Objekt, lässt Sie aber keine Änderungen vornehmen.

Delegation als Alternative zur Vererbung

In der objektorientierten Programmierung ist das Wiederverwenden und Erweitern von Code eine häufige Aufgabe. Es gibt zwei Hauptwege, um dies zu erreichen: Vererbung und Delegation.

Vererbung ist ein Mechanismus, bei dem eine Unterklasse Methoden und Attribute von einer Elternklasse erbt. Die Unterklasse kann einige dieser geerbten Methoden überschreiben, um ihre eigene Implementierung bereitzustellen.

Delegation hingegen bedeutet, dass ein Objekt ein anderes Objekt enthält und bestimmte Methodenaufrufe an dieses weiterleitet.

In diesem Schritt werden wir die Delegation als Alternative zur Vererbung untersuchen. Wir werden eine Klasse implementieren, die einen Teil ihres Verhaltens an ein anderes Objekt delegiert.

Einrichten eines Delegation-Beispiels

Zunächst müssen wir die Basisklasse einrichten, mit der unsere delegierende Klasse interagieren wird.

  1. Erstellen Sie eine neue Datei namens base_class.py im Verzeichnis /home/labex/project. Diese Datei wird eine Klasse namens Spam mit drei Methoden definieren: method_a, method_b und method_c. Jede Methode gibt eine Nachricht aus und gibt ein Ergebnis zurück. Hier ist der Code, der in base_class.py eingefügt werden soll:
class Spam:
    def method_a(self):
        print('Spam.method_a executed')
        return "Result from Spam.method_a"

    def method_b(self):
        print('Spam.method_b executed')
        return "Result from Spam.method_b"

    def method_c(self):
        print('Spam.method_c executed')
        return "Result from Spam.method_c"

Als Nächstes erstellen wir die delegierende Klasse.

  1. Erstellen Sie eine neue Datei namens delegator.py. In dieser Datei werden wir eine Klasse namens DelegatingSpam definieren, die einen Teil ihres Verhaltens an eine Instanz der Spam-Klasse delegiert.
from base_class import Spam

class DelegatingSpam:
    def __init__(self):
        ## Create an instance of Spam that we'll delegate to
        self._spam = Spam()

    def method_a(self):
        ## Override method_a but also call the original
        print('DelegatingSpam.method_a executed')
        result = self._spam.method_a()
        return f"Modified {result}"

    def method_c(self):
        ## Completely override method_c
        print('DelegatingSpam.method_c executed')
        return "Result from DelegatingSpam.method_c"

    def __getattr__(self, name):
        ## For any other attribute/method, delegate to self._spam
        print(f"Delegating {name} to the wrapped Spam object")
        return getattr(self._spam, name)

In der __init__-Methode erstellen we eine Instanz der Spam-Klasse. Die method_a-Methode überschreibt die ursprüngliche Methode, ruft aber auch die method_a der Spam-Klasse auf. Die method_c-Methode überschreibt die ursprüngliche Methode vollständig. Die __getattr__-Methode ist eine spezielle Methode in Python, die aufgerufen wird, wenn auf ein Attribut oder eine Methode zugegriffen wird, die in der DelegatingSpam-Klasse nicht existiert. Sie leitet dann den Aufruf an die Spam-Instanz weiter.

Jetzt erstellen wir eine Testdatei, um unsere Implementierung zu überprüfen.

  1. Erstellen Sie eine Testdatei namens test_delegation.py. Diese Datei wird eine Instanz der DelegatingSpam-Klasse erstellen und ihre Methoden aufrufen.
from delegator import DelegatingSpam

## Create a delegating object
spam = DelegatingSpam()

print("Calling method_a (overridden with delegation):")
result_a = spam.method_a()
print(f"Result: {result_a}\n")

print("Calling method_b (not defined in DelegatingSpam, delegated):")
result_b = spam.method_b()
print(f"Result: {result_b}\n")

print("Calling method_c (completely overridden):")
result_c = spam.method_c()
print(f"Result: {result_c}\n")

## Try accessing a non-existent method
try:
    print("Calling non-existent method_d:")
    spam.method_d()
except AttributeError as e:
    print(f"Error: {e}")

Schließlich führen wir das Testskript aus.

  1. Führen Sie das Testskript mit den folgenden Befehlen im Terminal aus:
cd /home/labex/project
python3 test_delegation.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Calling method_a (overridden with delegation):
DelegatingSpam.method_a executed
Spam.method_a executed
Result: Modified Result from Spam.method_a

Calling method_b (not defined in DelegatingSpam, delegated):
Delegating method_b to the wrapped Spam object
Spam.method_b executed
Result: Result from Spam.method_b

Calling method_c (completely overridden):
DelegatingSpam.method_c executed
Result: Result from DelegatingSpam.method_c

Calling non-existent method_d:
Delegating method_d to the wrapped Spam object
Error: 'Spam' object has no attribute 'method_d'

Delegation vs. Vererbung

Jetzt vergleichen wir die Delegation mit der traditionellen Vererbung.

  1. Erstellen Sie eine Datei namens inheritance_example.py. In dieser Datei werden wir eine Klasse namens InheritingSpam definieren, die von der Spam-Klasse erbt.
from base_class import Spam

class InheritingSpam(Spam):
    def method_a(self):
        ## Override method_a but also call the parent method
        print('InheritingSpam.method_a executed')
        result = super().method_a()
        return f"Modified {result}"

    def method_c(self):
        ## Completely override method_c
        print('InheritingSpam.method_c executed')
        return "Result from InheritingSpam.method_c"

Die InheritingSpam-Klasse überschreibt die method_a und method_c Methoden. In der method_a-Methode verwenden wir super(), um die method_a der Elternklasse aufzurufen.

Als Nächstes erstellen wir eine Testdatei für das Vererbungsbeispiel.

  1. Erstellen Sie eine Testdatei namens test_inheritance.py. Diese Datei wird eine Instanz der InheritingSpam-Klasse erstellen und ihre Methoden aufrufen.
from inheritance_example import InheritingSpam

## Create an inheriting object
spam = InheritingSpam()

print("Calling method_a (overridden with super call):")
result_a = spam.method_a()
print(f"Result: {result_a}\n")

print("Calling method_b (inherited from parent):")
result_b = spam.method_b()
print(f"Result: {result_b}\n")

print("Calling method_c (completely overridden):")
result_c = spam.method_c()
print(f"Result: {result_c}\n")

## Try accessing a non-existent method
try:
    print("Calling non-existent method_d:")
    spam.method_d()
except AttributeError as e:
    print(f"Error: {e}")

Schließlich führen wir den Vererbungstest aus.

  1. Führen Sie den Vererbungstest mit den folgenden Befehlen im Terminal aus:
cd /home/labex/project
python3 test_inheritance.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Calling method_a (overridden with super call):
InheritingSpam.method_a executed
Spam.method_a executed
Result: Modified Result from Spam.method_a

Calling method_b (inherited from parent):
Spam.method_b executed
Result: Result from Spam.method_b

Calling method_c (completely overridden):
InheritingSpam.method_c executed
Result: Result from InheritingSpam.method_c

Calling non-existent method_d:
Error: 'InheritingSpam' object has no attribute 'method_d'

Wichtige Unterschiede und Überlegungen

Schauen wir uns die Gemeinsamkeiten und Unterschiede zwischen Delegation und Vererbung an.

  1. Methodenüberschreibung: Sowohl Delegation als auch Vererbung ermöglichen es Ihnen, Methoden zu überschreiben, aber die Syntax ist unterschiedlich.

    • Bei der Delegation definieren Sie Ihre eigene Methode und entscheiden, ob Sie die Methode des umwickelten Objekts aufrufen möchten.
    • Bei der Vererbung definieren Sie Ihre eigene Methode und verwenden super(), um die Methode der Elternklasse aufzurufen.
  2. Methodenzugriff:

    • Bei der Delegation werden undefinierte Methoden über die __getattr__-Methode weitergeleitet.
    • Bei der Vererbung werden undefinierte Methoden automatisch geerbt.
  3. Typbeziehungen:

    • Bei der Delegation gibt isinstance(delegating_spam, Spam) False zurück, da das DelegatingSpam-Objekt keine Instanz der Spam-Klasse ist.
    • Bei der Vererbung gibt isinstance(inheriting_spam, Spam) True zurück, da die InheritingSpam-Klasse von der Spam-Klasse erbt.
  4. Einschränkungen: Die Delegation über __getattr__ funktioniert nicht mit speziellen Methoden wie __getitem__, __len__ usw. Diese Methoden müssten in der delegierenden Klasse explizit definiert werden.

Delegation ist besonders nützlich in folgenden Situationen:

  • Sie möchten das Verhalten eines Objekts anpassen, ohne seine Hierarchie zu beeinflussen.
  • Sie möchten Verhaltensweisen aus mehreren Objekten kombinieren, die keine gemeinsame Elternklasse haben.
  • Sie benötigen mehr Flexibilität als die Vererbung bietet.

Vererbung wird im Allgemeinen bevorzugt, wenn:

  • Die "ist-ein"-Beziehung klar ist (z.B. ein Auto ist ein Fahrzeug).
  • Sie die Typkompatibilität in Ihrem Code aufrechterhalten müssen.
  • Spezielle Methoden geerbt werden müssen.

Zusammenfassung

In diesem Lab haben Sie über mächtige Python - Mechanismen zur Anpassung des Attributzugriffs und Verhaltens gelernt. Sie haben untersucht, wie Sie __setattr__ verwenden können, um zu kontrollieren, welche Attribute an einem Objekt festgelegt werden können, und so einen kontrollierten Zugriff auf die Objekteigenschaften ermöglichen. Darüber hinaus haben Sie einen schreibgeschützten Proxy implementiert, um vorhandene Objekte zu umwickeln und so Modifikationen zu verhindern, während ihre Funktionalität erhalten bleibt.

Sie haben auch den Unterschied zwischen Delegation und Vererbung für die Codewiederverwendung und -anpassung untersucht. Durch die Verwendung von __getattr__ haben Sie gelernt, Methodenaufrufe an ein umwickeltes Objekt weiterzuleiten. Diese Techniken bieten flexible Möglichkeiten, das Objektverhalten über die Standardvererbung hinaus zu kontrollieren. Sie sind nützlich für die Erstellung kontrollierter Schnittstellen, die Implementierung von Zugriffseinschränkungen, das Hinzufügen von Querschnittsvorgängen und das Zusammensetzen von Verhalten aus mehreren Quellen. Das Verständnis dieser Muster hilft Ihnen, wartbareren und flexibleren Python - Code zu schreiben.