Metaklassen in Aktion

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 sich mit Metaklassen befassen, einer der mächtigsten und fortgeschrittensten Funktionen von Python. Metaklassen ermöglichen es Ihnen, die Klassen-Erstellung anzupassen und Ihnen die Kontrolle darüber zu geben, wie Klassen definiert und instanziiert werden. Sie werden Metaklassen anhand praktischer Beispiele erkunden.

Die Ziele dieses Labs sind es, zu verstehen, was Metaklassen sind und wie sie funktionieren, eine Metaklasse zu implementieren, um reale Programmierprobleme zu lösen und die praktischen Anwendungen von Metaklassen in Python zu erkunden. Die in diesem Lab modifizierten Dateien sind structure.py und validate.py.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ModulesandPackagesGroup(["Modules and Packages"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/FunctionsGroup -.-> python/function_definition("Function Definition") python/ModulesandPackagesGroup -.-> python/importing_modules("Importing Modules") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") subgraph Lab Skills python/function_definition -.-> lab-132521{{"Metaklassen in Aktion"}} python/importing_modules -.-> lab-132521{{"Metaklassen in Aktion"}} python/classes_objects -.-> lab-132521{{"Metaklassen in Aktion"}} end

Das Problem verstehen

Bevor wir mit der Erkundung von Metaklassen beginnen, ist es wichtig, das Problem zu verstehen, das wir lösen möchten. In der Programmierung müssen wir oft Strukturen mit bestimmten Typen für ihre Attribute erstellen. In unserer vorherigen Arbeit haben wir ein System für typüberprüfte Strukturen entwickelt. Dieses System ermöglicht es uns, Klassen zu definieren, bei denen jedes Attribut einen bestimmten Typ hat und die den Attributen zugewiesenen Werte gemäß diesem Typ validiert werden.

Hier ist ein Beispiel, wie wir dieses System verwendet haben, um eine Stock-Klasse zu erstellen:

from validate import String, PositiveInteger, PositiveFloat
from structure import Structure

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

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

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

In diesem Code importieren wir zunächst die Validator-Typen (String, PositiveInteger, PositiveFloat) aus dem validate-Modul und die Structure-Klasse aus dem structure-Modul. Dann definieren wir die Stock-Klasse, die von Structure erbt. Innerhalb der Stock-Klasse definieren wir Attribute mit bestimmten Validator-Typen. Beispielsweise muss das name-Attribut eine Zeichenkette sein, shares muss eine positive Ganzzahl sein und price muss eine positive Fließkommazahl sein.

Es gibt jedoch ein Problem mit diesem Ansatz. Wir müssen alle Validator-Typen am Anfang unserer Datei importieren. Wenn wir in einem realen Szenario immer mehr Validator-Typen hinzufügen, können diese Importe sehr lang und schwierig zu verwalten werden. Dies könnte uns dazu verleiten, from validate import * zu verwenden, was im Allgemeinen als schlechter Stil angesehen wird, da es Namenskonflikte verursachen und den Code weniger lesbar machen kann.

Um unseren Ausgangspunkt zu verstehen, schauen wir uns die Structure-Klasse an. Sie müssen die Datei structure.py im Editor öffnen und ihren Inhalt untersuchen. Dies hilft Ihnen zu verstehen, wie die grundlegende Strukturverarbeitung implementiert ist, bevor wir Metaklassen-Funktionalität hinzufügen.

code structure.py

Wenn Sie die Datei öffnen, sehen Sie eine grundlegende Implementierung der Structure-Klasse. Diese Klasse ist für die Initialisierung der Attribute verantwortlich, hat aber noch keine Metaklassen-Funktionalität.

Als Nächstes untersuchen wir die Validator-Klassen. Diese Klassen sind in der Datei validate.py definiert. Sie haben bereits Descriptor-Funktionalität, was bedeutet, dass sie steuern können, wie Attribute zugegriffen und gesetzt werden. Wir müssen sie jedoch verbessern, um das Importproblem zu lösen, das wir zuvor besprochen haben.

code validate.py

Indem Sie sich diese Validator-Klassen ansehen, verstehen Sie besser, wie der Validierungsprozess funktioniert und welche Änderungen wir vornehmen müssen, um unseren Code zu verbessern.

✨ Lösung prüfen und üben

Sammeln von Validator-Typen

In Python sind Validatoren Klassen, die uns helfen, sicherzustellen, dass Daten bestimmte Kriterien erfüllen. Unsere erste Aufgabe in diesem Experiment besteht darin, die Basisklasse Validator so zu modifizieren, dass sie alle ihre Unterklassen sammeln kann. Warum müssen wir das tun? Nun, indem wir alle Validator-Unterklassen sammeln, können wir einen Namensraum erstellen, der alle Validator-Typen enthält. Später werden wir diesen Namensraum in die Structure-Klasse einfügen, was es uns erleichtern wird, verschiedene Validatoren zu verwalten und zu verwenden.

Jetzt beginnen wir mit der Arbeit am Code. Öffnen Sie die Datei validate.py. Sie können den folgenden Befehl im Terminal verwenden, um sie zu öffnen:

code validate.py

Sobald die Datei geöffnet ist, müssen wir der Validator-Klasse ein Klassen-Level-Wörterbuch und eine __init_subclass__()-Methode hinzufügen. Das Klassen-Level-Wörterbuch wird verwendet, um alle Validator-Unterklassen zu speichern, und die __init_subclass__()-Methode ist eine spezielle Methode in Python, die jedes Mal aufgerufen wird, wenn eine Unterklasse der aktuellen Klasse definiert wird.

Fügen Sie den folgenden Code direkt nach der Klassendefinition der Validator-Klasse hinzu:

## Add this to the Validator class in validate.py
validators = {}  ## Dictionary to collect all validator subclasses

@classmethod
def __init_subclass__(cls):
    """Register each validator subclass in the validators dictionary"""
    Validator.validators[cls.__name__] = cls

Nachdem Sie den Code hinzugefügt haben, sollte Ihre modifizierte Validator-Klasse nun so aussehen:

class Validator:
    validators = {}  ## Dictionary to collect all validator subclasses

    @classmethod
    def __init_subclass__(cls):
        """Register each validator subclass in the validators dictionary"""
        Validator.validators[cls.__name__] = cls

    def __set_name__(self, owner, name):
        self.name = name

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

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

    def validate(self, value):
        pass

Jetzt wird jedes Mal, wenn ein neuer Validator-Typ definiert wird, wie String oder PositiveInteger, Python automatisch die __init_subclass__()-Methode aufrufen. Diese Methode fügt dann die neue Validator-Unterklasse dem validators-Wörterbuch hinzu, wobei der Klassenname als Schlüssel verwendet wird.

Lassen Sie uns testen, ob unser Code funktioniert. Wir erstellen ein einfaches Python-Skript, um den Inhalt des validators-Wörterbuchs zu überprüfen. Sie können den folgenden Befehl im Terminal ausführen:

python3 -c "from validate import Validator; print(Validator.validators)"

Wenn alles korrekt funktioniert, sollten Sie eine Ausgabe ähnlich der folgenden sehen, die alle Validator-Typen und ihre entsprechenden Klassen anzeigt:

{'Typed': <class 'validate.Typed'>, 'Positive': <class 'validate.Positive'>, 'NonEmpty': <class 'validate.NonEmpty'>, 'String': <class 'validate.String'>, 'Integer': <class 'validate.Integer'>, 'Float': <class 'validate.Float'>, 'PositiveInteger': <class 'validate.PositiveInteger'>, 'PositiveFloat': <class 'validate.PositiveFloat'>, 'NonEmptyString': <class 'validate.NonEmptyString'>}

Jetzt, da wir ein Wörterbuch haben, das alle unsere Validator-Typen enthält, können wir es im nächsten Schritt verwenden, um unsere Metaklasse zu erstellen.

Erstellen der StructureMeta-Metaklasse

Jetzt sprechen wir darüber, was wir als Nächstes tun werden. Wir haben einen Weg gefunden, alle Validator-Typen zu sammeln. Unser nächster Schritt besteht darin, eine Metaklasse zu erstellen. Aber was genau ist eine Metaklasse? In Python ist eine Metaklasse eine besondere Art von Klasse. Ihre Instanzen sind selbst Klassen. Das bedeutet, dass eine Metaklasse steuern kann, wie eine Klasse erstellt wird. Sie kann den Namensraum verwalten, in dem die Klassenattribute definiert werden.

In unserer Situation möchten wir eine Metaklasse erstellen, die die Validator-Typen verfügbar macht, wenn wir eine Structure-Unterklasse definieren. Wir möchten nicht jedes Mal diese Validator-Typen explizit importieren müssen.

Beginnen wir damit, die Datei structure.py erneut zu öffnen. Sie können den folgenden Befehl verwenden, um sie zu öffnen:

code structure.py

Sobald die Datei geöffnet ist, müssen wir etwas Code oben in der Datei, vor der Definition der Structure-Klasse, hinzufügen. Dieser Code wird unsere Metaklasse definieren.

from validate import Validator
from collections import ChainMap

class StructureMeta(type):
    @classmethod
    def __prepare__(meta, clsname, bases):
        """Prepare the namespace for the class being defined"""
        return ChainMap({}, Validator.validators)

    @staticmethod
    def __new__(meta, name, bases, methods):
        """Create the new class using only the local namespace"""
        methods = methods.maps[0]  ## Extract the local namespace
        return super().__new__(meta, name, bases, methods)

Jetzt, da wir die Metaklasse definiert haben, müssen wir die Structure-Klasse modifizieren, um sie zu verwenden. Auf diese Weise profitiert jede Klasse, die von Structure erbt, von der Funktionalität der Metaklasse.

class Structure(metaclass=StructureMeta):
    _fields = []

    def __init__(self, *args, **kwargs):
        if len(args) > len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')

        ## Set all of the positional arguments
        for name, val in zip(self._fields, args):
            setattr(self, name, val)

        ## Set the remaining keyword arguments
        for name, val in kwargs.items():
            if name not in self._fields:
                raise TypeError(f'Invalid argument: {name}')
            setattr(self, name, val)

    def __repr__(self):
        values = [getattr(self, name) for name in self._fields]
        args_str = ','.join(repr(val) for val in values)
        return f'{type(self).__name__}({args_str})'

Lassen Sie uns analysieren, was dieser Code tut:

  1. Die __prepare__()-Methode ist eine spezielle Methode in Python. Sie wird aufgerufen, bevor die Klasse erstellt wird. Ihre Aufgabe besteht darin, den Namensraum vorzubereiten, in dem die Klassenattribute definiert werden. Wir verwenden hier ChainMap. ChainMap ist ein nützliches Werkzeug, das ein geschichtetes Wörterbuch erstellt. In unserem Fall enthält es unsere Validator-Typen, wodurch sie im Klassen-Namensraum zugänglich sind.

  2. Die __new__()-Methode ist für die Erstellung der neuen Klasse verantwortlich. Wir extrahieren nur den lokalen Namensraum, der das erste Wörterbuch in der ChainMap ist. Wir verwerfen das Validator-Wörterbuch, da wir die Validator-Typen bereits im Namensraum verfügbar gemacht haben.

Mit dieser Einrichtung hat jede Klasse, die von Structure erbt, Zugang zu allen Validator-Typen, ohne dass es erforderlich ist, sie explizit zu importieren.

Jetzt testen wir unsere Implementierung. Wir erstellen eine Stock-Klasse, die von unserer erweiterten Structure-Basisklasse erbt.

cat > stock.py << EOF
from structure import Structure

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

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

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

Wenn unsere Metaklasse korrekt funktioniert, sollten wir in der Lage sein, die Stock-Klasse zu definieren, ohne die Validator-Typen zu importieren. Dies liegt daran, dass die Metaklasse sie bereits im Namensraum verfügbar gemacht hat.

Testen unserer Implementierung

Nachdem wir unsere Metaklasse implementiert und die Structure-Klasse modifiziert haben, ist es Zeit, unsere Implementierung zu testen. Testing ist von entscheidender Bedeutung, da es uns hilft, sicherzustellen, dass alles korrekt funktioniert. Durch das Ausführen von Tests können wir potenzielle Probleme frühzeitig erkennen und sicherstellen, dass unser Code wie erwartet verhält.

Zunächst führen wir die Unit-Tests aus, um zu sehen, ob unsere Stock-Klasse wie erwartet funktioniert. Unit-Tests sind kleine, isolierte Tests, die einzelne Teile unseres Codes überprüfen. In diesem Fall möchten wir sicherstellen, dass die Stock-Klasse korrekt funktioniert. Um die Unit-Tests auszuführen, verwenden wir den folgenden Befehl im Terminal:

python3 teststock.py

Wenn alles korrekt funktioniert, sollten alle Tests ohne Fehler bestanden werden. Wenn die Tests erfolgreich ablaufen, sollte die Ausgabe in etwa so aussehen:

........
----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

Die Punkte repräsentieren jeden Test, der bestanden wurde, und das abschließende OK gibt an, dass alle Tests erfolgreich waren.

Jetzt testen wir unsere Stock-Klasse mit einigen echten Daten und der Tabellenformatierungsfunktion. Dies gibt uns ein realitätsnäheres Szenario, um zu sehen, wie unsere Stock-Klasse mit Daten interagiert und wie die Tabellenformatierung funktioniert. Wir verwenden den folgenden Befehl im Terminal:

python3 -c "
from stock import Stock
from reader import read_csv_as_instances
from tableformat import create_formatter, print_table

## Read portfolio data into Stock instances
portfolio = read_csv_as_instances('portfolio.csv', Stock)
print('Portfolio:')
print(portfolio)

## Format and print the portfolio data
print('\nFormatted table:')
formatter = create_formatter('text')
print_table(portfolio, ['name', 'shares', 'price'], formatter)
"

In diesem Code importieren wir zunächst die erforderlichen Klassen und Funktionen. Dann lesen wir Daten aus einer CSV-Datei in Stock-Instanzen. Danach geben wir die Portfolio-Daten aus, formatieren sie in eine Tabelle und geben die formatierte Tabelle aus.

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Portfolio:
[Stock('AA',100,32.2), Stock('IBM',50,91.1), Stock('CAT',150,83.44), Stock('MSFT',200,51.23), Stock('GE',95,40.37), Stock('MSFT',50,65.1), Stock('IBM',100,70.44)]

Formatted table:
      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

Nehmen Sie sich einen Moment Zeit, um zu würdigen, was wir erreicht haben:

  1. Wir haben einen Mechanismus erstellt, um automatisch alle Validator-Typen zu sammeln. Das bedeutet, dass wir nicht manuell alle Validatoren verfolgen müssen, was uns Zeit spart und die Wahrscheinlichkeit von Fehlern verringert.
  2. Wir haben eine Metaklasse implementiert, die diese Typen in den Namensraum von Structure-Unterklassen einfügt. Dies ermöglicht es den Unterklassen, diese Validatoren zu verwenden, ohne sie explizit importieren zu müssen.
  3. Wir haben die Notwendigkeit für explizite Importe von Validator-Typen beseitigt. Dies macht unseren Code sauberer und leichter lesbar.
  4. All dies geschieht im Hintergrund, wodurch der Code für die Definition neuer Strukturen sauber und einfach wird.

Die endgültige stock.py-Datei ist bemerkenswert sauber im Vergleich zu dem, was sie ohne unsere Metaklasse gewesen wäre:

from structure import Structure

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

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

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

Ohne die Notwendigkeit, die Validator-Typen direkt zu importieren, ist der Code kompakter und leichter zu warten. Dies ist ein großartiges Beispiel dafür, wie Metaklassen die Qualität unseres Codes verbessern können.

Zusammenfassung

In diesem Lab haben Sie gelernt, wie Sie die Macht von Metaklassen in Python nutzen können. Zunächst haben Sie die Herausforderung beim Verwalten von Importen für Validator-Typen verstanden. Dann haben Sie die Validator-Klasse modifiziert, um ihre Unterklassen automatisch zu sammeln, und eine StructureMeta-Metaklasse erstellt, um Validator-Typen in Klassen-Namensräume einzufügen. Schließlich haben Sie die Implementierung mit einer Stock-Klasse getestet und so die Notwendigkeit für explizite Importe beseitigt.

Metaklassen, eine fortgeschrittene Python-Funktion, ermöglichen die Anpassung des Klassen-Erstellungsprozesses. Obwohl sie sparsam eingesetzt werden sollten, bieten sie elegante Lösungen für bestimmte Probleme, wie in diesem Lab gezeigt wurde. Durch die Verwendung einer Metaklasse haben Sie den Code zur Definition von Strukturen mit validierten Attributen vereinfacht, die Notwendigkeit für explizite Importe von Validator-Typen entfernt und eine wartbarere und elegantere API erstellt. Dieses auf Metaklassen basierte Muster zur Namensraum-Injektion kann auch in anderen Szenarien angewendet werden, um eine vereinfachte Benutzer-API zu schaffen.