Zirkuläre und dynamische Modulimporte

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 zwei entscheidende Konzepte im Zusammenhang mit Importen in Python kennenlernen. Modulimporte in Python können manchmal zu komplexen Abhängigkeiten führen, was zu Fehlern oder ineffizienten Code-Strukturen führt. Zirkuläre Importe, bei denen zwei oder mehr Module sich gegenseitig importieren, schaffen eine Abhängigkeits-Schleife, die Probleme verursachen kann, wenn sie nicht richtig verwaltet werden.

Sie werden auch dynamische Importe untersuchen, die es ermöglichen, Module zur Laufzeit statt beim Start des Programms zu laden. Dies bietet Flexibilität und hilft, Import-bezogene Probleme zu vermeiden. Die Ziele dieses Labs sind es, die Probleme bei zirkulären Importen zu verstehen, Lösungen zu implementieren, um sie zu vermeiden, und zu lernen, wie man dynamische Modulimporte effektiv nutzt.

Das Import-Problem verstehen

Beginnen wir damit, zu verstehen, was Modulimporte sind. In Python verwenden Sie die import-Anweisung, wenn Sie Funktionen, Klassen oder Variablen aus einer anderen Datei (Modul) verwenden möchten. Allerdings kann die Art und Weise, wie Sie Ihre Importe strukturieren, zu verschiedenen Problemen führen.

Nun werden wir uns ein Beispiel für eine problematische Modulstruktur ansehen. Der Code in tableformat/formatter.py enthält Importe, die über die gesamte Datei verteilt sind. Dies mag zunächst nicht wie ein großes Problem erscheinen, aber es führt zu Wartungs- und Abhängigkeitsproblemen.

Öffnen Sie zunächst den Dateiexplorer der WebIDE und navigieren Sie zum Verzeichnis structly. Wir werden ein paar Befehle ausführen, um die aktuelle Struktur des Projekts zu verstehen. Der cd-Befehl wird verwendet, um das aktuelle Arbeitsverzeichnis zu ändern, und der ls -la-Befehl listet alle Dateien und Verzeichnisse im aktuellen Verzeichnis auf, einschließlich versteckter Dateien.

cd ~/project/structly
ls -la

Dadurch werden Ihnen die Dateien im Projektverzeichnis angezeigt. Jetzt werden wir uns eine der problematischen Dateien mit dem cat-Befehl ansehen, der den Inhalt einer Datei anzeigt.

cat tableformat/formatter.py

Sie sollten Code ähnlich dem folgenden sehen:

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

def create_formatter(name, column_formats=None, upper_headers=False):
    if name == 'text':
        formatter_cls = TextTableFormatter
    elif name == 'csv':
        formatter_cls = CSVTableFormatter
    elif name == 'html':
        formatter_cls = HTMLTableFormatter
    else:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

Beachten Sie die Platzierung der Importanweisungen in der Mitte der Datei. Dies ist aus mehreren Gründen problematisch:

  1. Es macht den Code schwieriger zu lesen und zu warten. Wenn Sie sich eine Datei ansehen, erwarten Sie, alle Importe am Anfang zu sehen, damit Sie schnell verstehen können, von welchen externen Modulen die Datei abhängt.
  2. Es kann zu zirkulären Importproblemen führen. Zirkuläre Importe treten auf, wenn zwei oder mehr Module voneinander abhängen, was Fehler verursachen und dazu führen kann, dass Ihr Code unerwartet verhält.
  3. Es bricht die Python-Konvention, alle Importe am Anfang einer Datei zu platzieren. Die Einhaltung von Konventionen macht Ihren Code lesbarer und leichter für andere Entwickler zu verstehen.

In den folgenden Schritten werden wir diese Probleme ausführlicher untersuchen und lernen, wie wir sie beheben können.

Untersuchung von zirkulären Importen

Ein zirkulärer Import tritt auf, wenn zwei oder mehr Module voneinander abhängen. Genauer gesagt, wenn Modul A Modul B importiert und Modul B ebenfalls Modul A importiert, entweder direkt oder indirekt. Dies schafft eine Abhängigkeits-Schleife, die das Import-System von Python nicht richtig auflösen kann. Einfacher ausgedrückt: Python gerät in eine Schleife, in der es versucht, herauszufinden, welches Modul zuerst importiert werden soll, und dies kann zu Fehlern in Ihrem Programm führen.

Lassen Sie uns mit unserem Code experimentieren, um zu sehen, wie zirkuläre Importe Probleme verursachen können.

Zunächst werden wir das Aktienprogramm ausführen, um zu prüfen, ob es mit der aktuellen Struktur funktioniert. Dieser Schritt hilft uns, eine Basislinie zu erstellen und das Programm in der erwarteten Weise laufen zu sehen, bevor wir irgendwelche Änderungen vornehmen.

cd ~/project/structly
python3 stock.py

Das Programm sollte korrekt laufen und die Aktiendaten in einer formatierten Tabelle anzeigen. Wenn dies der Fall ist, bedeutet dies, dass die aktuelle Code-Struktur ohne zirkuläre Importprobleme funktioniert.

Jetzt werden wir die Datei formatter.py ändern. Normalerweise ist es eine gute Praxis, Importe an den Anfang einer Datei zu verschieben. Dies macht den Code besser organisiert und leichter auf einen Blick zu verstehen.

cd ~/project/structly

Öffnen Sie tableformat/formatter.py in der WebIDE. Wir werden die folgenden Importe an den Anfang der Datei verschieben, direkt nach den vorhandenen Importen. Diese Importe sind für verschiedene Tabellenformatierer, wie Text, CSV und HTML.

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Der Anfang der Datei sollte jetzt wie folgt aussehen:

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

Speichern Sie die Datei und versuchen Sie, das Aktienprogramm erneut auszuführen.

python3 stock.py

Sie sollten eine Fehlermeldung darüber sehen, dass TableFormatter nicht definiert ist. Dies ist ein eindeutiges Zeichen für ein zirkuläres Importproblem.

Das Problem tritt auf, weil die folgenden Ereignisse nacheinander eintreten:

  1. formatter.py versucht, TextTableFormatter aus formats/text.py zu importieren.
  2. formats/text.py importiert TableFormatter aus formatter.py.
  3. Wenn Python versucht, diese Importe aufzulösen, gerät es in eine Schleife, weil es nicht entscheiden kann, welches Modul zuerst vollständig importiert werden soll.

Lassen Sie uns unsere Änderungen rückgängig machen, damit das Programm wieder funktioniert. Bearbeiten Sie tableformat/formatter.py und verschieben Sie die Importe zurück an ihre ursprüngliche Stelle (nach der Definition der TableFormatter-Klasse).

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Führen Sie das Programm erneut aus, um zu bestätigen, dass es funktioniert.

python3 stock.py

Dies zeigt, dass obwohl es in Bezug auf die Code-Organisation nicht die beste Praxis ist, Importe in der Mitte der Datei zu haben, dies getan wurde, um ein zirkuläres Importproblem zu vermeiden. In den nächsten Schritten werden wir bessere Lösungen untersuchen.

Implementierung der Subklassen-Registrierung

In der Programmierung können zirkuläre Importe ein kniffliges Problem darstellen. Anstatt die Formatierer-Klassen direkt zu importieren, können wir ein Registrierungsmuster verwenden. In diesem Muster registrieren sich die Subklassen bei ihrer Basisklasse. Dies ist eine gängige und effektive Methode, um zirkuläre Importe zu vermeiden.

Zunächst verstehen wir, wie wir den Modulnamen einer Klasse herausfinden können. Der Modulname ist wichtig, da wir ihn in unserem Registrierungsmuster verwenden werden. Dazu führen wir einen Python-Befehl im Terminal aus.

cd ~/project/structly
python3 -c "from structly.tableformat.formats.text import TextTableFormatter; print(TextTableFormatter.__module__); print(TextTableFormatter.__module__.split('.')[-1])"

Wenn Sie diesen Befehl ausführen, sehen Sie eine Ausgabe wie diese:

structly.tableformat.formats.text
text

Diese Ausgabe zeigt, dass wir den Namen des Moduls direkt aus der Klasse extrahieren können. Wir werden diesen Modulnamen später verwenden, um die Subklassen zu registrieren.

Jetzt ändern wir die TableFormatter-Klasse in der Datei tableformat/formatter.py, um einen Registrierungsmechanismus hinzuzufügen. Öffnen Sie diese Datei in der WebIDE. Wir fügen der TableFormatter-Klasse etwas Code hinzu. Dieser Code wird uns helfen, die Subklassen automatisch zu registrieren.

class TableFormatter(ABC):
    _formats = { }  ## Dictionary to store registered formatters

    @classmethod
    def __init_subclass__(cls):
        name = cls.__module__.split('.')[-1]
        TableFormatter._formats[name] = cls

    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

Die Methode __init_subclass__ ist eine spezielle Methode in Python. Sie wird aufgerufen, wenn immer eine Subklasse von TableFormatter erstellt wird. In dieser Methode extrahieren wir den Modulnamen der Subklasse und verwenden ihn als Schlüssel, um die Subklasse im _formats-Dictionary zu registrieren.

Als Nächstes müssen wir die Funktion create_formatter ändern, um das Registrierungs-Dictionary zu verwenden. Diese Funktion ist dafür verantwortlich, den passenden Formatierer basierend auf dem gegebenen Namen zu erstellen.

def create_formatter(name, column_formats=None, upper_headers=False):
    formatter_cls = TableFormatter._formats.get(name)
    if not formatter_cls:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

Nachdem Sie diese Änderungen vorgenommen haben, speichern Sie die Datei. Dann testen wir, ob das Programm noch funktioniert. Wir führen das Skript stock.py aus.

python3 stock.py

Wenn das Programm korrekt läuft, bedeutet dies, dass unsere Änderungen nichts kaputt gemacht haben. Jetzt schauen wir uns den Inhalt des _formats-Dictionarys an, um zu sehen, wie die Registrierung funktioniert.

python3 -c "from structly.tableformat.formatter import TableFormatter; print(TableFormatter._formats)"

Sie sollten eine Ausgabe wie diese sehen:

{'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>, 'csv': <class 'structly.tableformat.formats.csv.CSVTableFormatter'>, 'html': <class 'structly.tableformat.formats.html.HTMLTableFormatter'>}

Diese Ausgabe bestätigt, dass unsere Subklassen korrekt im _formats-Dictionary registriert werden. Allerdings haben wir immer noch einige Importe in der Mitte der Datei. Im nächsten Schritt werden wir dieses Problem mithilfe von dynamischen Importen beheben.

✨ Lösung prüfen und üben

Verwendung dynamischer Importe

In der Programmierung werden Importe verwendet, um Code aus anderen Modulen einzubinden, damit wir deren Funktionalität nutzen können. Manchmal kann es jedoch dazu führen, dass der Code etwas unübersichtlich und schwer zu verstehen wird, wenn Importe mitten in einer Datei stehen. In diesem Abschnitt lernen wir, wie wir dynamische Importe verwenden können, um dieses Problem zu lösen. Dynamische Importe sind eine leistungsstarke Funktion, die es uns ermöglicht, Module zur Laufzeit zu laden. Das bedeutet, dass wir ein Modul erst laden, wenn wir es tatsächlich benötigen.

Zunächst müssen wir die Importanweisungen entfernen, die derzeit nach der TableFormatter-Klasse platziert sind. Diese Importe sind statische Importe, die beim Start des Programms geladen werden. Öffnen Sie dazu die Datei tableformat/formatter.py in der WebIDE. Sobald Sie die Datei geöffnet haben, suchen Sie die folgenden Zeilen und löschen Sie sie:

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Wenn Sie jetzt versuchen, das Programm auszuführen, indem Sie den folgenden Befehl im Terminal ausführen:

python3 stock.py

wird das Programm fehlschlagen. Der Grund dafür ist, dass die Formatierer nicht im _formats-Dictionary registriert werden. Sie werden eine Fehlermeldung über ein unbekanntes Format sehen. Dies liegt daran, dass das Programm die Formatierer-Klassen nicht finden kann, die es benötigt, um richtig zu funktionieren.

Um dieses Problem zu beheben, ändern weir die create_formatter-Funktion. Das Ziel ist es, das erforderliche Modul dynamisch zu importieren, wenn es benötigt wird. Aktualisieren Sie die Funktion wie unten gezeigt:

def create_formatter(name, column_formats=None, upper_headers=False):
    if name not in TableFormatter._formats:
        __import__(f'{__package__}.formats.{name}')

    formatter_cls = TableFormatter._formats.get(name)
    if not formatter_cls:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

Die wichtigste Zeile in dieser Funktion ist:

__import__(f'{__package__}.formats.{name}')

Diese Zeile importiert das Modul dynamisch basierend auf dem Formatnamen. Wenn das Modul importiert wird, registriert sich seine Subklasse von TableFormatter automatisch. Dies ist dank der __init_subclass__-Methode möglich, die wir zuvor hinzugefügt haben. Diese Methode ist eine spezielle Python-Methode, die aufgerufen wird, wenn eine Subklasse erstellt wird. In unserem Fall wird sie verwendet, um die Formatierer-Klasse zu registrieren.

Nachdem Sie diese Änderungen vorgenommen haben, speichern Sie die Datei. Führen Sie dann das Programm erneut aus, indem Sie den folgenden Befehl verwenden:

python3 stock.py

Das Programm sollte jetzt korrekt funktionieren, obwohl wir die statischen Importe entfernt haben. Um zu überprüfen, ob der dynamische Import wie erwartet funktioniert, leeren wir das _formats-Dictionary und rufen dann die create_formatter-Funktion auf. Führen Sie den folgenden Befehl im Terminal aus:

python3 -c "from structly.tableformat.formatter import TableFormatter, create_formatter; TableFormatter._formats.clear(); print('Before:', TableFormatter._formats); create_formatter('text'); print('After:', TableFormatter._formats)"

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Before: {}
After: {'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>}

Diese Ausgabe bestätigt, dass der dynamische Import das Modul lädt und die Formatierer-Klasse bei Bedarf registriert.

Durch die Verwendung dynamischer Importe und der Klassenregistrierung haben wir eine sauberere und wartbarere Code-Struktur erstellt. Hier sind die Vorteile:

  1. Alle Importe befinden sich jetzt am Anfang der Datei, was den Python-Konventionen entspricht. Dies macht den Code leichter zu lesen und zu verstehen.
  2. Wir haben zirkuläre Importe eliminiert. Zirkuläre Importe können in einem Programm Probleme verursachen, wie z. B. unendliche Schleifen oder schwer zu debuggende Fehler.
  3. Der Code ist flexibler. Jetzt können wir neue Formatierer hinzufügen, ohne die create_formatter-Funktion zu ändern. Dies ist in einer realen Anwendung sehr nützlich, in der möglicherweise im Laufe der Zeit neue Funktionen hinzugefügt werden.

Dieses Muster der Verwendung dynamischer Importe und Klassenregistrierung wird häufig in Plug-In-Systemen und Frameworks eingesetzt. In diesen Systemen müssen Komponenten dynamisch auf der Grundlage der Benutzeranforderungen oder der Programmvoraussetzungen geladen werden.

✨ Lösung prüfen und üben

Zusammenfassung

In diesem Lab haben Sie wichtige Konzepte und Techniken zum Importieren von Python-Modulen kennengelernt. Zunächst haben Sie zirkuläre Importe untersucht und verstanden, wie zirkuläre Abhängigkeiten zwischen Modulen Probleme verursachen können und warum eine sorgfältige Behandlung erforderlich ist, um sie zu vermeiden. Zweitens haben Sie die Subklassen-Registrierung implementiert, ein Muster, bei dem Subklassen sich bei ihrer Basisklasse registrieren, wodurch der direkte Import von Subklassen entfällt.

Sie haben auch die Funktion __import__() für dynamische Importe verwendet, um Module nur bei Bedarf zur Laufzeit zu laden. Dies macht den Code flexibler und hilft, zirkuläre Abhängigkeiten zu vermeiden. Diese Techniken sind unerlässlich für die Erstellung wartbarer Python-Pakete mit komplexen Modulbeziehungen und werden häufig in Frameworks und Bibliotheken eingesetzt. Die Anwendung dieser Muster in Ihren Projekten kann Ihnen helfen, modularere, erweiterbare und wartbarere Code-Strukturen zu erstellen.