Geltungsbereichsregeln und Tricks

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 die Geltungsbereichsregeln (scoping rules) von Python kennen und erkunden fortgeschrittene Techniken für die Arbeit mit Geltungsbereichen. Das Verständnis von Geltungsbereichen in Python ist entscheidend für das Schreiben von sauberem und wartbarem Code und hilft, unerwartetes Verhalten zu vermeiden.

Die Ziele dieses Labs umfassen das detaillierte Verständnis der Geltungsbereichsregeln von Python, das Erlernen praktischer Techniken für die Initialisierung von Klassen, die Implementierung eines flexiblen Objekt-Initialisierungssystems und die Anwendung von Frame-Inspektions-Techniken zur Vereinfachung des Codes. Sie werden mit den Dateien structure.py und stock.py arbeiten.

Das Problem bei der Klasseninitialisierung verstehen

In der Welt der Programmierung sind Klassen ein grundlegendes Konzept, das es Ihnen ermöglicht, benutzerdefinierte Datentypen zu erstellen. In früheren Übungen haben Sie möglicherweise eine Structure-Klasse erstellt. Diese Klasse ist ein nützliches Werkzeug, um Datenstrukturen einfach zu definieren. Eine Datenstruktur ist eine Möglichkeit, Daten zu organisieren und zu speichern, damit sie effizient abgerufen und verwendet werden können. Die Structure-Klasse als Basisklasse kümmert sich um die Initialisierung von Attributen anhand einer vordefinierten Liste von Feldnamen. Attribute sind Variablen, die zu einem Objekt gehören, und Feldnamen sind die Namen, die wir diesen Attributen geben.

Schauen wir uns die aktuelle Implementierung der Structure-Klasse genauer an. Dazu müssen wir die Datei structure.py im Code-Editor öffnen. Diese Datei enthält den Code für die Structure-Klasse. Hier sind die Befehle, um in das Projektverzeichnis zu navigieren und die Datei zu öffnen:

cd ~/project
code structure.py

Die Structure-Klasse bietet einen grundlegenden Rahmen für die Definition einfacher Datenstrukturen. Wenn wir eine Unterklasse wie die Stock-Klasse erstellen, können wir die spezifischen Felder definieren, die wir für diese Unterklasse benötigen. Eine Unterklasse erbt die Eigenschaften und Methoden ihrer Basisklasse, in diesem Fall die Structure-Klasse. Beispielsweise definieren wir in der Stock-Klasse die Felder name, shares und price:

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

Jetzt öffnen wir die Datei stock.py, um zu sehen, wie die Stock-Klasse im Kontext des gesamten Codes implementiert ist. Diese Datei enthält wahrscheinlich den Code, der die Stock-Klasse verwendet und mit ihr interagiert. Verwenden Sie den folgenden Befehl, um die Datei zu öffnen:

code stock.py

Obwohl dieser Ansatz mit der Structure-Klasse und ihren Unterklassen funktioniert, hat er mehrere Einschränkungen. Um diese Probleme zu identifizieren, werden wir den Python-Interpreter starten und untersuchen, wie die Stock-Klasse verhält. Der folgende Befehl importiert die Stock-Klasse und zeigt ihre Hilfsinformationen an:

python3 -c "from stock import Stock; help(Stock)"

Wenn Sie diesen Befehl ausführen, werden Sie feststellen, dass die im Hilfsoutput angezeigte Signatur nicht sehr hilfreich ist. Anstelle der tatsächlichen Parameternamen wie name, shares und price wird nur *args angezeigt. Dieser Mangel an klaren Parameternamen macht es für Benutzer schwierig, zu verstehen, wie sie korrekt eine Instanz der Stock-Klasse erstellen können.

Versuchen wir auch, eine Stock-Instanz mit Schlüsselwortargumenten zu erstellen. Schlüsselwortargumente ermöglichen es Ihnen, die Werte für Parameter anhand ihrer Namen anzugeben, was den Code lesbarer machen kann. Führen Sie den folgenden Befehl aus:

python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"

Sie sollten eine Fehlermeldung wie diese erhalten:

TypeError: __init__() got an unexpected keyword argument 'name'

Dieser Fehler tritt auf, weil unsere aktuelle __init__-Methode, die für die Initialisierung von Objekten der Stock-Klasse verantwortlich ist, keine Schlüsselwortargumente verarbeitet. Sie akzeptiert nur Positionsargumente, was bedeutet, dass Sie die Werte in einer bestimmten Reihenfolge angeben müssen, ohne die Parameternamen zu verwenden. Dies ist eine Einschränkung, die wir in diesem Lab beheben möchten.

In diesem Lab werden wir verschiedene Ansätze untersuchen, um unsere Structure-Klasse flexibler und benutzerfreundlicher zu machen. Dadurch können wir die Benutzerfreundlichkeit der Stock-Klasse und anderer Unterklassen von Structure verbessern.

Verwendung von locals() zum Zugriff auf Funktionsargumente

In Python ist das Verständnis von Variablenbereichen (variable scopes) von entscheidender Bedeutung. Der Geltungsbereich einer Variablen bestimmt, wo im Code auf sie zugegriffen werden kann. Python bietet eine eingebaute Funktion namens locals(), die für Anfänger sehr nützlich ist, um Geltungsbereiche zu verstehen. Die locals()-Funktion gibt ein Wörterbuch zurück, das alle lokalen Variablen im aktuellen Geltungsbereich enthält. Dies kann äußerst nützlich sein, wenn Sie Funktionsargumente untersuchen möchten, da es Ihnen einen klaren Überblick darüber gibt, welche Variablen in einem bestimmten Teil Ihres Codes verfügbar sind.

Lassen Sie uns ein einfaches Experiment im Python-Interpreter durchführen, um zu sehen, wie dies funktioniert. Zunächst müssen wir in das Projektverzeichnis navigieren und den Python-Interpreter starten. Sie können dies tun, indem Sie die folgenden Befehle in Ihrem Terminal ausführen:

cd ~/project
python3

Sobald Sie in der interaktiven Python-Shell sind, werden wir eine Stock-Klasse definieren. Eine Klasse in Python ist wie ein Bauplan für die Erstellung von Objekten. In dieser Klasse werden wir die spezielle __init__-Methode verwenden. Die __init__-Methode ist ein Konstruktor in Python, was bedeutet, dass sie automatisch aufgerufen wird, wenn ein Objekt der Klasse erstellt wird. Innerhalb dieser __init__-Methode werden wir die locals()-Funktion verwenden, um alle lokalen Variablen auszugeben.

class Stock:
    def __init__(self, name, shares, price):
        print(locals())

Jetzt erstellen wir eine Instanz dieser Stock-Klasse. Eine Instanz ist ein tatsächliches Objekt, das aus dem Klassenbauplan erstellt wird. Wir werden einige Werte für die Parameter name, shares und price übergeben.

s = Stock('GOOG', 100, 490.1)

Wenn Sie diesen Code ausführen, sollten Sie eine Ausgabe ähnlich der folgenden sehen:

{'self': <__main__.Stock object at 0x...>, 'name': 'GOOG', 'shares': 100, 'price': 490.1}

Diese Ausgabe zeigt, dass locals() uns ein Wörterbuch gibt, das alle lokalen Variablen in der __init__-Methode enthält. Die self-Referenz ist eine spezielle Variable in Python-Klassen, die auf die Instanz der Klasse selbst verweist. Die anderen Variablen sind die Parameterwerte, die wir beim Erstellen des Stock-Objekts übergeben haben.

Wir können diese locals()-Funktionalität nutzen, um Objektattribute automatisch zu initialisieren. Attribute sind Variablen, die einem Objekt zugeordnet sind. Lassen Sie uns eine Hilfsfunktion definieren und unsere Stock-Klasse modifizieren.

def _init(locs):
    self = locs.pop('self')
    for name, val in locs.items():
        setattr(self, name, val)

class Stock:
    def __init__(self, name, shares, price):
        _init(locals())

Die _init-Funktion nimmt das Wörterbuch der lokalen Variablen, das von locals() erhalten wurde. Sie entfernt zunächst die self-Referenz aus dem Wörterbuch mithilfe der pop-Methode. Dann iteriert sie über die verbleibenden Schlüssel-Wert-Paare im Wörterbuch und verwendet die setattr-Funktion, um jede Variable als Attribut des Objekts festzulegen.

Jetzt testen wir diese Implementierung sowohl mit Positions- als auch mit Schlüsselwortargumenten. Positionsargumente werden in der Reihenfolge übergeben, in der sie in der Funktionssignatur definiert sind, während Schlüsselwortargumente mit den angegebenen Parameternamen übergeben werden.

## Test with positional arguments
s1 = Stock('GOOG', 100, 490.1)
print(s1.name, s1.shares, s1.price)

## Test with keyword arguments
s2 = Stock(name='AAPL', shares=50, price=125.3)
print(s2.name, s2.shares, s2.price)

Beide Ansätze sollten jetzt funktionieren! Die _init-Funktion ermöglicht es uns, sowohl Positions- als auch Schlüsselwortargumente nahtlos zu verarbeiten. Sie bewahrt auch die Parameternamen in der Funktionssignatur, was die help()-Ausgabe nützlicher macht. Die help()-Funktion in Python liefert Informationen über Funktionen, Klassen und Module, und wenn die Parameternamen intakt sind, wird diese Information sinnvoller.

Wenn Sie mit dem Experiment fertig sind, können Sie den Python-Interpreter beenden, indem Sie den folgenden Befehl ausführen:

exit()

Untersuchung der Stack-Frame-Inspektion

Der von uns verwendete Ansatz _init(locals()) funktioniert, hat aber einen Nachteil. Jedes Mal, wenn wir eine __init__-Methode definieren, müssen wir explizit locals() aufrufen. Dies kann etwas umständlich werden, insbesondere wenn es um mehrere Klassen geht. Glücklicherweise können wir unseren Code sauberer und effizienter gestalten, indem wir die Stack-Frame-Inspektion nutzen. Diese Technik ermöglicht es uns, automatisch auf die lokalen Variablen des Aufrufers zuzugreifen, ohne locals() explizit aufrufen zu müssen.

Lassen Sie uns diese Technik im Python-Interpreter untersuchen. Zunächst öffnen Sie Ihr Terminal und navigieren Sie in das Projektverzeichnis. Starten Sie dann den Python-Interpreter. Sie können dies tun, indem Sie die folgenden Befehle ausführen:

cd ~/project
python3

Jetzt, da wir im Python-Interpreter sind, müssen wir das sys-Modul importieren. Das sys-Modul bietet Zugang zu einigen Variablen, die vom Python-Interpreter verwendet oder verwaltet werden. Wir werden es nutzen, um auf die Stack-Frame-Informationen zuzugreifen.

import sys

Als Nächstes definieren wir eine verbesserte Version unserer _init()-Funktion. Diese neue Version wird direkt auf den Frame des Aufrufers zugreifen, sodass es nicht mehr erforderlich ist, locals() explizit zu übergeben.

def _init():
    ## Get the caller's frame (1 level up in the call stack)
    frame = sys._getframe(1)

    ## Get the local variables from that frame
    locs = frame.f_locals

    ## Extract self and set other variables as attributes
    self = locs.pop('self')
    for name, val in locs.items():
        setattr(self, name, val)

In diesem Code ruft sys._getframe(1) das Frame-Objekt der aufrufenden Funktion ab. Das Argument 1 bedeutet, dass wir eine Ebene höher im Aufrufstack suchen. Sobald wir das Frame-Objekt haben, können wir seine lokalen Variablen über frame.f_locals zugreifen. Dies gibt uns ein Wörterbuch aller lokalen Variablen im Geltungsbereich des Aufrufers. Wir extrahieren dann die self-Variable und setzen die verbleibenden Variablen als Attribute des self-Objekts.

Jetzt testen wir diese neue _init()-Funktion mit einer neuen Version unserer Stock-Klasse.

class Stock:
    def __init__(self, name, shares, price):
        _init()  ## No need to pass locals() anymore!

## Test it
s = Stock('GOOG', 100, 490.1)
print(s.name, s.shares, s.price)

## Also works with keyword arguments
s = Stock(name='AAPL', shares=50, price=125.3)
print(s.name, s.shares, s.price)

Wie Sie sehen können, muss die __init__-Methode nicht mehr explizit locals() übergeben. Dies macht unseren Code sauberer und leichter lesbar aus Sicht des Aufrufers.

Wie die Stack-Frame-Inspektion funktioniert

Wenn Sie sys._getframe(1) aufrufen, gibt Python das Frame-Objekt zurück, das den Ausführungsframe des Aufrufers repräsentiert. Das Argument 1 bedeutet "eine Ebene über dem aktuellen Frame" (der aufrufenden Funktion).

Ein Frame-Objekt enthält wichtige Informationen über den Ausführungskontext. Dies umfasst die aktuell ausgeführte Funktion, die lokalen Variablen in dieser Funktion und die aktuell ausgeführte Zeilennummer.

Durch den Zugriff auf frame.f_locals erhalten wir ein Wörterbuch aller lokalen Variablen im Geltungsbereich des Aufrufers. Dies ist ähnlich wie das, was locals() zurückgeben würde, wenn es direkt aus diesem Geltungsbereich aufgerufen würde.

Diese Technik ist recht leistungsstark, sollte aber mit Vorsicht angewendet werden. Sie wird im Allgemeinen als fortgeschrittenes Python-Feature angesehen und kann etwas "magisch" erscheinen, da sie über die normalen Geltungsbereichsgrenzen von Python hinausgreift.

Sobald Sie mit der Untersuchung der Stack-Frame-Inspektion fertig sind, können Sie den Python-Interpreter beenden, indem Sie den folgenden Befehl ausführen:

exit()

Implementierung der erweiterten Initialisierung in der Structure-Klasse

Wir haben gerade zwei leistungsstarke Techniken zum Zugriff auf Funktionsargumente gelernt. Jetzt werden wir diese Techniken nutzen, um unsere Structure-Klasse zu aktualisieren. Zunächst verstehen wir, warum wir dies tun. Diese Techniken machen unsere Klasse flexibler und einfacher zu verwenden, insbesondere wenn es um verschiedene Argumenttypen geht.

Öffnen Sie die Datei structure.py im Code-Editor. Sie können dies tun, indem Sie die folgenden Befehle im Terminal ausführen. Der cd-Befehl wechselt das Verzeichnis in den Projektordner, und der code-Befehl öffnet die Datei structure.py im Code-Editor.

cd ~/project
code structure.py

Ersetzen Sie den Inhalt der Datei durch den folgenden Code. Dieser Code definiert eine Structure-Klasse mit mehreren Methoden. Gehen wir durch jeden Teil, um zu verstehen, was er tut.

import sys

class Structure:
    _fields = ()

    @staticmethod
    def _init():
        ## Get the caller's frame (the __init__ method that called this)
        frame = sys._getframe(1)

        ## Get the local variables from that frame
        locs = frame.f_locals

        ## Extract self and set other variables as attributes
        self = locs.pop('self')
        for name, val in locs.items():
            setattr(self, name, val)

    def __repr__(self):
        values = ', '.join(f'{name}={getattr(self, name)!r}' for name in self._fields)
        return f'{type(self).__name__}({values})'

    def __setattr__(self, name, value):
        if name.startswith('_') or name in self._fields:
            super().__setattr__(name, value)
        else:
            raise AttributeError(f'{type(self).__name__!r} has no attribute {name!r}')

Hier ist, was wir im Code getan haben:

  1. Wir haben die alte __init__()-Methode entfernt. Da Unterklassen ihre eigenen __init__-Methoden definieren werden, benötigen wir die alte nicht mehr.
  2. Wir haben eine neue statische _init()-Methode hinzugefügt. Diese Methode nutzt die Frame-Inspektion, um automatisch alle Parameter als Attribute zu erfassen und zu setzen. Die Frame-Inspektion ermöglicht es uns, auf die lokalen Variablen der aufrufenden Methode zuzugreifen.
  3. Wir haben die __repr__()-Methode beibehalten. Diese Methode liefert eine schöne String-Repräsentation des Objekts, die für das Debugging und das Ausgeben nützlich ist.
  4. Wir haben eine __setattr__()-Methode hinzugefügt. Diese Methode erzwingt die Attributvalidierung und stellt sicher, dass nur gültige Attribute für das Objekt festgelegt werden können.

Jetzt aktualisieren wir die Stock-Klasse. Öffnen Sie die Datei stock.py mit dem folgenden Befehl:

code stock.py

Ersetzen Sie ihren Inhalt durch den folgenden Code:

from structure import Structure

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

    def __init__(self, name, shares, price):
        self._init()  ## This magically captures and sets all parameters!

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

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

Der wichtigste Unterschied hier ist, dass unsere __init__-Methode jetzt self._init() aufruft, anstatt jedes Attribut manuell zu setzen. Die _init()-Methode nutzt die Frame-Inspektion, um automatisch alle Parameter als Attribute zu erfassen und zu setzen. Dies macht den Code kompakter und leichter zu warten.

Lassen Sie uns unsere Implementierung testen, indem wir die Unittests ausführen. Die Unittests helfen uns, sicherzustellen, dass unser Code wie erwartet funktioniert. Führen Sie die folgenden Befehle im Terminal aus:

cd ~/project
python3 teststock.py

Sie sollten sehen, dass alle Tests bestanden werden, einschließlich des Tests für Schlüsselwortargumente, der zuvor fehlgeschlagen ist. Dies bedeutet, dass unsere Implementierung korrekt funktioniert.

Lassen Sie uns auch die Hilfedokumentation für unsere Stock-Klasse überprüfen. Die Hilfedokumentation liefert Informationen über die Klasse und ihre Methoden. Führen Sie den folgenden Befehl im Terminal aus:

python3 -c "from stock import Stock; help(Stock)"

Jetzt sollten Sie eine korrekte Signatur für die __init__-Methode sehen, die alle Parameternamen anzeigt. Dies erleichtert es anderen Entwicklern, zu verstehen, wie die Klasse verwendet werden kann.

Schließlich testen wir interaktiv, ob Schlüsselwortargumente wie erwartet funktionieren. Führen Sie den folgenden Befehl im Terminal aus:

python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"

Sie sollten sehen, dass das Stock-Objekt ordnungsgemäß mit den angegebenen Attributen erstellt wird. Dies bestätigt, dass unser Klasseninitialisierungssystem Schlüsselwortargumente unterstützt.

Mit dieser Implementierung haben wir ein viel flexibleres und benutzerfreundlicheres Klasseninitialisierungssystem erreicht, das:

  1. Die richtigen Funktionssignaturen in der Dokumentation beibehält, was es Entwicklern erleichtert, zu verstehen, wie die Klasse verwendet wird.
  2. Sowohl Positions- als auch Schlüsselwortargumente unterstützt, was mehr Flexibilität bei der Objekterstellung bietet.
  3. Minimalen Boilerplate-Code in Unterklassen erfordert, wodurch die Menge an zu schreibendem Code reduziert wird.
✨ Lösung prüfen und üben

Zusammenfassung

In diesem Lab haben Sie die Geltungsbereichsregeln (Scoping Rules) von Python und einige leistungsstarke Techniken zur Verwaltung von Geltungsbereichen kennengelernt. Zunächst haben Sie untersucht, wie Sie die locals()-Funktion nutzen können, um auf alle lokalen Variablen innerhalb einer Funktion zuzugreifen. Zweitens haben Sie gelernt, wie Sie Stack-Frames mit sys._getframe() untersuchen können, um auf die lokalen Variablen des Aufrufers zuzugreifen.

Sie haben diese Techniken auch angewandt, um ein flexibles Klasseninitialisierungssystem zu erstellen. Dieses System erfasst automatisch Funktionsparameter und setzt sie als Objektattribute, erhält die richtigen Funktionssignaturen in der Dokumentation und unterstützt sowohl Positions- als auch Schlüsselwortargumente. Diese Techniken zeigen die Flexibilität und Introspektionsfähigkeiten von Python. Obwohl die Frame-Inspektion eine fortgeschrittene Technik ist, die mit Vorsicht angewendet werden sollte, kann sie bei angemessener Verwendung effektiv Boilerplate-Code reduzieren. Das Verständnis der Geltungsbereichsregeln und dieser fortgeschrittenen Techniken bereitet Sie mit mehr Werkzeugen aus, um saubereren und besser wartbaren Python-Code zu schreiben.