Mehr über Closures erfahren

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 mehr über Closures in Python lernen. Closures sind ein mächtiges Programmierkonzept, das Funktionen ermöglicht, Variablen aus ihrem umgebenden Geltungsbereich zu merken und darauf zuzugreifen, auch nachdem die äußere Funktion ihre Ausführung abgeschlossen hat.

Sie werden auch verstehen, wie Closures als Datenstruktur fungieren, sie als Codegenerator erkunden und entdecken, wie Sie Typüberprüfung mit Closures implementieren können. Dieses Lab wird Ihnen helfen, einige der ungewöhnlicheren und mächtigeren Aspekte von Python-Closures aufzudecken.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/FunctionsGroup -.-> python/scope("Scope") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("Raising Exceptions") subgraph Lab Skills python/conditional_statements -.-> lab-132506{{"Mehr über Closures erfahren"}} python/function_definition -.-> lab-132506{{"Mehr über Closures erfahren"}} python/arguments_return -.-> lab-132506{{"Mehr über Closures erfahren"}} python/scope -.-> lab-132506{{"Mehr über Closures erfahren"}} python/classes_objects -.-> lab-132506{{"Mehr über Closures erfahren"}} python/raising_exceptions -.-> lab-132506{{"Mehr über Closures erfahren"}} end

Closures als Datenstruktur

In Python bieten Closures eine mächtige Möglichkeit, Daten zu kapseln. Kapselung bedeutet, Daten privat zu halten und den Zugriff darauf zu kontrollieren. Mit Closures können Sie Funktionen erstellen, die private Daten verwalten und ändern, ohne Klassen oder globale Variablen verwenden zu müssen. Globale Variablen können von überall in Ihrem Code aus zugegriffen und geändert werden, was zu unerwartetem Verhalten führen kann. Klassen hingegen erfordern eine komplexere Struktur. Closures bieten eine einfachere Alternative für die Datenkapselung.

Erstellen wir eine Datei namens counter.py, um dieses Konzept zu demonstrieren:

  1. Öffnen Sie die WebIDE und erstellen Sie eine neue Datei namens counter.py im Verzeichnis /home/labex/project. Hier werden wir den Code schreiben, der unseren auf einem Closure basierenden Zähler definiert.

  2. Fügen Sie der Datei folgenden Code hinzu:

def counter(value):
    """
    Create a counter with increment and decrement functions.

    Args:
        value: Initial value of the counter

    Returns:
        Two functions: one to increment the counter, one to decrement it
    """
    def incr():
        nonlocal value
        value += 1
        return value

    def decr():
        nonlocal value
        value -= 1
        return value

    return incr, decr

In diesem Code definieren wir eine Funktion namens counter(). Diese Funktion nimmt einen Anfangswert value als Argument. Innerhalb der counter()-Funktion definieren wir zwei innere Funktionen: incr() und decr(). Diese inneren Funktionen teilen sich den Zugriff auf dieselbe value-Variable. Das Schlüsselwort nonlocal wird verwendet, um Python mitzuteilen, dass wir die value-Variable aus dem umgebenden Geltungsbereich (der counter()-Funktion) ändern möchten. Ohne das nonlocal-Schlüsselwort würde Python eine neue lokale Variable innerhalb der inneren Funktionen erstellen, anstatt die value-Variable aus dem äußeren Geltungsbereich zu ändern.

  1. Jetzt erstellen wir eine Testdatei, um dies in Aktion zu sehen. Erstellen Sie eine neue Datei namens test_counter.py mit folgendem Inhalt:
from counter import counter

## Create a counter starting at 0
up, down = counter(0)

## Increment the counter several times
print("Incrementing the counter:")
print(up())  ## Should print 1
print(up())  ## Should print 2
print(up())  ## Should print 3

## Decrement the counter
print("\nDecrementing the counter:")
print(down())  ## Should print 2
print(down())  ## Should print 1

In dieser Testdatei importieren wir zunächst die counter()-Funktion aus der counter.py-Datei. Dann erstellen wir einen Zähler, der bei 0 beginnt, indem wir counter(0) aufrufen und die zurückgegebenen Funktionen in up und down entpacken. Anschließend rufen wir die up()-Funktion mehrmals auf, um den Zähler zu erhöhen und die Ergebnisse auszugeben. Danach rufen wir die down()-Funktion auf, um den Zähler zu verringern und die Ergebnisse auszugeben.

  1. Führen Sie die Testdatei aus, indem Sie den folgenden Befehl im Terminal ausführen:
python3 test_counter.py

Sie sollten die folgende Ausgabe sehen:

Incrementing the counter:
1
2
3

Decrementing the counter:
2
1

Beachten Sie, dass hier keine Klassendefinition beteiligt ist. Die up()- und down()-Funktionen manipulieren einen gemeinsamen Wert, der weder eine globale Variable noch ein Instanzattribut ist. Dieser Wert wird im Closure gespeichert, wodurch er nur für die von counter() zurückgegebenen Funktionen zugänglich ist.

Dies ist ein Beispiel dafür, wie Closures als Datenstruktur verwendet werden können. Die eingeschlossene Variable value wird zwischen Funktionsaufrufen aufrechterhalten und ist für die Funktionen, die darauf zugreifen, privat. Dies bedeutet, dass kein anderer Teil Ihres Codes direkt auf diese value-Variable zugreifen oder sie ändern kann, was ein gewisses Maß an Datenschutz bietet.

Closures als Codegenerator

In diesem Schritt werden wir lernen, wie Closures verwendet werden können, um Code dynamisch zu generieren. Insbesondere werden wir ein Typüberprüfungssystem für Klassenattribute mit Hilfe von Closures erstellen.

Zunächst verstehen wir, was Closures sind. Ein Closure ist ein Funktionsobjekt, das Werte aus dem umgebenden Geltungsbereich (Scope) behält, auch wenn diese nicht mehr im Speicher vorhanden sind. In Python werden Closures erstellt, wenn eine verschachtelte Funktion auf einen Wert aus ihrer umgebenden Funktion verweist.

Jetzt beginnen wir mit der Implementierung unseres Typüberprüfungssystems.

  1. Erstellen Sie eine neue Datei namens typedproperty.py im Verzeichnis /home/labex/project mit folgendem Code:
## typedproperty.py

def typedproperty(name, expected_type):
    """
    Create a property with type checking.

    Args:
        name: The name of the property
        expected_type: The expected type of the property value

    Returns:
        A property object that performs type checking
    """
    private_name = '_' + name

    @property
    def value(self):
        return getattr(self, private_name)

    @value.setter
    def value(self, val):
        if not isinstance(val, expected_type):
            raise TypeError(f'Expected {expected_type}')
        setattr(self, private_name, val)

    return value

In diesem Code ist die typedproperty-Funktion ein Closure. Sie nimmt zwei Argumente entgegen: name und expected_type. Der @property-Decorator wird verwendet, um eine Getter-Methode für das Attribut zu erstellen, die den Wert des privaten Attributs abruft. Der @value.setter-Decorator erstellt eine Setter-Methode, die überprüft, ob der zu setzende Wert vom erwarteten Typ ist. Wenn nicht, wird ein TypeError ausgelöst.

  1. Jetzt erstellen wir eine Klasse, die diese typisierten Attribute verwendet. Erstellen Sie eine Datei namens stock.py mit folgendem Code:
from typedproperty import typedproperty

class Stock:
    """A class representing a stock with type-checked attributes."""

    name = typedproperty('name', str)
    shares = typedproperty('shares', int)
    price = typedproperty('price', float)

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

In der Stock-Klasse verwenden wir die typedproperty-Funktion, um typüberprüfte Attribute für name, shares und price zu erstellen. Wenn wir eine Instanz der Stock-Klasse erstellen, wird die Typüberprüfung automatisch angewendet.

  1. Erstellen wir eine Testdatei, um dies in Aktion zu sehen. Erstellen Sie eine Datei namens test_stock.py mit folgendem Code:
from stock import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try to set an attribute with the wrong type
try:
    s.shares = "hundred"  ## This should raise a TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

In dieser Testdatei erstellen wir zunächst ein Stock-Objekt mit korrekten Typen. Dann versuchen wir, das shares-Attribut auf einen String zu setzen, was einen TypeError auslösen sollte, da der erwartete Typ eine Ganzzahl ist.

  1. Führen Sie die Testdatei aus:
python3 test_stock.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'int'>

Diese Ausgabe zeigt, dass die Typüberprüfung korrekt funktioniert.

  1. Jetzt erweitern wir die typedproperty.py-Datei, indem wir bequeme Funktionen für häufige Typen hinzufügen. Fügen Sie folgenden Code ans Ende der Datei hinzu:
def String(name):
    """Create a string property with type checking."""
    return typedproperty(name, str)

def Integer(name):
    """Create an integer property with type checking."""
    return typedproperty(name, int)

def Float(name):
    """Create a float property with type checking."""
    return typedproperty(name, float)

Diese Funktionen sind einfach Wrapper um die typedproperty-Funktion, was es einfacher macht, Attribute für häufige Typen zu erstellen.

  1. Erstellen Sie eine neue Datei namens stock_enhanced.py, die diese bequemen Funktionen verwendet:
from typedproperty import String, Integer, Float

class Stock:
    """A class representing a stock with type-checked attributes."""

    name = String('name')
    shares = Integer('shares')
    price = Float('price')

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

Diese Stock-Klasse verwendet die bequemen Funktionen, um typüberprüfte Attribute zu erstellen, was den Code lesbarer macht.

  1. Erstellen Sie eine Testdatei test_stock_enhanced.py, um die erweiterte Version zu testen:
from stock_enhanced import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try to set an attribute with the wrong type
try:
    s.price = "490.1"  ## This should raise a TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

Diese Testdatei ist ähnlich der vorherigen, aber sie testet die erweiterte Stock-Klasse.

  1. Führen Sie den Test aus:
python3 test_stock_enhanced.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'float'>

In diesem Schritt haben wir gezeigt, wie Closures verwendet werden können, um Code zu generieren. Die typedproperty-Funktion erstellt Eigenschaftsobjekte, die Typüberprüfung durchführen, und die String, Integer und Float-Funktionen erstellen spezialisierte Eigenschaften für häufige Typen.

✨ Lösung prüfen und üben

Elimination von Eigenschaftsnamen mit Descriptoren

Im vorherigen Schritt mussten wir bei der Erstellung von typisierten Eigenschaften die Eigenschaftsnamen explizit angeben. Dies ist redundant, da die Eigenschaftsnamen bereits in der Klassendefinition festgelegt sind. In diesem Schritt werden wir Descriptor (Beschreiber) verwenden, um diese Redundanz zu beseitigen.

Ein Descriptor in Python ist ein spezielles Objekt, das steuert, wie der Zugriff auf Attribute funktioniert. Wenn Sie die __set_name__-Methode in einem Descriptor implementieren, kann dieser automatisch den Attributnamen aus der Klassendefinition abrufen.

Beginnen wir damit, eine neue Datei zu erstellen.

  1. Erstellen Sie eine neue Datei namens improved_typedproperty.py mit folgendem Code:
## improved_typedproperty.py

class TypedProperty:
    """
    A descriptor that performs type checking.

    This descriptor automatically captures the attribute name from the class definition.
    """
    def __init__(self, expected_type):
        self.expected_type = expected_type
        self.name = None

    def __set_name__(self, owner, name):
        ## This method is called when the descriptor is assigned to a class attribute
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f'Expected {self.expected_type}')
        instance.__dict__[self.name] = value

## Convenience functions
def String():
    """Create a string property with type checking."""
    return TypedProperty(str)

def Integer():
    """Create an integer property with type checking."""
    return TypedProperty(int)

def Float():
    """Create a float property with type checking."""
    return TypedProperty(float)

Dieser Code definiert eine Descriptor-Klasse namens TypedProperty, die den Typ von Werten überprüft, die an Attribute zugewiesen werden. Die __set_name__-Methode wird automatisch aufgerufen, wenn der Descriptor einem Klassenattribut zugewiesen wird. Dadurch kann der Descriptor den Attributnamen erfassen, ohne dass wir ihn manuell angeben müssen.

Als Nächstes erstellen wir eine Klasse, die diese verbesserten typisierten Eigenschaften verwendet.

  1. Erstellen Sie eine neue Datei namens stock_improved.py, die die verbesserten typisierten Eigenschaften verwendet:
from improved_typedproperty import String, Integer, Float

class Stock:
    """A class representing a stock with type-checked attributes."""

    ## No need to specify property names anymore
    name = String()
    shares = Integer()
    price = Float()

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

Beachten Sie, dass wir die Eigenschaftsnamen nicht mehr angeben müssen, wenn wir die typisierten Eigenschaften erstellen. Der Descriptor wird automatisch den Attributnamen aus der Klassendefinition abrufen.

Jetzt testen wir unsere verbesserte Klasse.

  1. Erstellen Sie eine Testdatei test_stock_improved.py, um die verbesserte Version zu testen:
from stock_improved import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try setting attributes with wrong types
try:
    s.name = 123  ## Should raise TypeError
    print("Name type check failed")
except TypeError as e:
    print(f"Name type check succeeded: {e}")

try:
    s.shares = "hundred"  ## Should raise TypeError
    print("Shares type check failed")
except TypeError as e:
    print(f"Shares type check succeeded: {e}")

try:
    s.price = "490.1"  ## Should raise TypeError
    print("Price type check failed")
except TypeError as e:
    print(f"Price type check succeeded: {e}")

Schließlich führen wir den Test aus, um zu sehen, ob alles wie erwartet funktioniert.

  1. Führen Sie den Test aus:
python3 test_stock_improved.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Name type check succeeded: Expected <class 'str'>
Shares type check succeeded: Expected <class 'int'>
Price type check succeeded: Expected <class 'float'>

In diesem Schritt haben wir unser Typüberprüfungssystem verbessert, indem wir Descriptor und die __set_name__-Methode verwendet haben. Dies beseitigt die redundante Angabe der Eigenschaftsnamen, macht den Code kürzer und verringert die Wahrscheinlichkeit von Fehlern.

Die __set_name__-Methode ist ein sehr nützliches Merkmal von Descriptor. Sie ermöglicht es ihnen, automatisch Informationen darüber zu sammeln, wie sie in einer Klassendefinition verwendet werden. Dies kann genutzt werden, um APIs zu erstellen, die einfacher zu verstehen und zu verwenden sind.

Zusammenfassung

In diesem Lab haben Sie erweiterte Aspekte von Closures in Python kennengelernt. Zunächst haben Sie untersucht, wie Closures als Datenstruktur verwendet werden können. Sie können Daten kapseln und es Funktionen ermöglichen, ihren Zustand zwischen Aufrufen beizubehalten, ohne sich auf Klassen oder globale Variablen zu verlassen. Zweitens haben Sie gesehen, wie Closures als Codegenerator fungieren können, indem sie Eigenschaftsobjekte mit Typüberprüfung generieren, was einen funktionaleren Ansatz für die Attributvalidierung ermöglicht.

Sie haben auch entdeckt, wie Sie das Descriptor - Protokoll und die __set_name__ - Methode nutzen können, um elegante typüberprüfende Attribute zu erstellen, die automatisch ihre Namen aus Klassendefinitionen erfassen. Diese Techniken zeigen die Stärke und Flexibilität von Closures auf und ermöglichen es Ihnen, komplexe Verhaltensweisen kompakt zu implementieren. Das Verständnis von Closures und Descriptor gibt Ihnen weitere Werkzeuge für die Erstellung von wartbarem und robustem Python - Code.