0%

Ein Programm über mehrere Dateien organisieren · Übung

Paketordner und zirkuläre Importe

Drei Module liegen neben main.py. Bei dieser Größe ist ein flaches Projekt in Ordnung. Wächst es auf zwanzig, hilft der Ordner nicht mehr: models.py, attempts.py, report.py, report_html.py, scoring.py, scoring_rules.py, ohne erkennbare Gruppierung.

Ein Paket ist ein Ordner mit Modulen und die nächste Stufe derselben Idee. Ein Modul gruppiert zusammengehörige Definitionen, ein Paket zusammengehörige Module.

Ein Paket erstellen

quizapp/
    __init__.py
    models.py
    scoring.py
main.py

quizapp ist ein reguläres Paket, weil der Ordner __init__.py enthält. Python unterstützt auch Namensraumpakete ohne diese Datei. Reguläre Pakete sind aber die ausdrückliche, vorhersehbare Form dieses Projekts. Ihre Module werden mit einem Punkt erreicht:

from quizapp.models import Question
from quizapp import scoring

Der Punkt in quizapp.models ist das Ordnertrennzeichen in Pythons Schreibweise für „innerhalb“.

Wozu __init__.py dient

Es kennzeichnet den Ordner als reguläres Paket und läuft beim ersten Import des Pakets. Meistens sollte es leer sein. Eine leere Datei ist dort völlig richtig und sehr üblich.

Eine häufige Verwendung ist festzulegen, was das Paket anbietet:

# quizapp/__init__.py
from quizapp.models import Question, Quiz

Mit dieser Zeile können Nutzer des Pakets from quizapp import Question schreiben, ohne das interne Modul der Klasse zu kennen. Das ist ein echter Vorteil und eine echte Verpflichtung: Du hast diesen Namen zugesichert. Ein Umzug der Klasse zwischen Modulen darf ihn nicht beschädigen.

Beginne mit einem leeren __init__.py. Ergänze es, wenn du einen Grund hast.

Was bewirkt ein leeres __init__.py?

Absolute Importe im eigenen Projekt

Innerhalb von quizapp/scoring.py wird der Zugriff auf das benachbarte models.py von der obersten Projektebene aus geschrieben:

from quizapp.models import Question

Nicht from models import Question, das außerhalb des Pakets suchen würde, und nicht from .models import Question, die relative Form. Relative Importe funktionieren, und du wirst ihnen in fremdem Code begegnen. Die absolute Form ist für ein Projekt dieser Größe klarer, weil die Zeile überall gleich gelesen wird.

Das dabei entstehende Problem

Wenn zwei Module einander importieren, heißt das zirkulärer Import. Dieses Importproblem trifft Menschen besonders häufig.

# quizapp/models.py
from quizapp.scoring import grade_letter      # models needs scoring

# quizapp/scoring.py
from quizapp.models import Question           # scoring needs models

Python beginnt, models zu importieren, erreicht Zeile eins und wechselt zum Import von scoring. scoring erreicht Zeile eins und fordert models an. Das wird bereits importiert und ist deshalb halb fertig: Der Name existiert, doch Question ist noch nicht definiert. Das Ergebnis ist ein ImportError mit einer Meldung über ein teilweise initialisiertes Modul. Die Meldung zeigt auf die Datei, die zufällig an zweiter Stelle war.

Verwirrend ist, dass beide Dateien für sich genommen korrekt sind. Der Fehler liegt in der Form der Abhängigkeit, nicht in einer einzelnen Zeile.

Vor der Reparatur importiert quizapp.models das Modul quizapp.scoring, während quizapp.scoring das Modul quizapp.models importiert. So entsteht ein roter Kreis. Nach der Reparatur importiert scoring nur noch in eine Richtung aus models, und models hat keinen Rückimport.

Den Kreis auflösen

Der Kreis weist fast immer auf ein echtes Problem hin. Gute Lösungen verbessern deshalb den Entwurf:

Eine der beiden Richtungen ist falsch. Das ist die übliche Antwort. Braucht models wirklich scoring, oder wurde eine Note an der falschen Stelle berechnet? Den unpassenden Import zu entfernen löst den Kreis auf und verbessert den Entwurf.

Der gemeinsame Teil braucht ein eigenes Modul. Wenn beide etwas tatsächlich benötigen, verschiebe es in ein drittes Modul, das beide importieren. Aus dem Kreis wird eine gerichtete Anordnung.

Der Import wird nur innerhalb einer Funktion gebraucht. Wenn import vom Dateianfang in die verwendende Funktion wandert, verzögert sich der Import, bis beide Module geladen sind. Das funktioniert, verdient aber einen kritischen Blick: Es versteckt die Abhängigkeit beim Lesen des Dateianfangs und bedeutet meist, dass eine der ersten beiden Lösungen die passende gewesen wäre.

Die Übung enthält einen echten Kreis, auf den die erste Lösung zutrifft. Die Arbeitsumgebung liefert außerdem das vollständige Bewertungsmodul des Pakets und ein paketinternes Berichtsmodul für den Abschluss. Deine einzige Aufgabe hier ist, die Richtung zwischen Bewertung und Modellen zu reparieren.

Aufgabe

Diese Lektion liefert ein Paket quizapp mit einem absichtlich zirkulären Import. Submit macht das beschädigte Paket sichtbar, obwohl Run noch die früheren flachen Module verwendet.

quizapp/scoring.py importiert Question aus quizapp.models. Das ist richtig: Die Bewertung braucht die Domäne. quizapp/models.py importiert grade_letter aus quizapp.scoring und schließt damit den Kreis. Keines der Module kann fertig laden.

Repariere das, indem du den unpassenden Import entfernst. Frage nach der tatsächlichen Richtung: Muss ein Question etwas über Notenbuchstaben wissen?

Question.grade_for(response) hat den falschen Import verursacht. Es gehört überhaupt nicht zu einer Frage. Lösche es deshalb. Eine Frage meldet, ob eine Antwort richtig ist. Welchem Buchstaben ein Anteil entspricht, entscheidet scoring, und scoring fragt die Frage bereits.

Lass alles andere in quizapp/models.py unverändert. Question, Quiz und Attempt behalten das Verhalten des gesamten Kapitels. Das Paket enthält die vollständige Domäne, aus der die nächste Lektion importiert.

Das vorgegebene quizapp/scoring.py enthält die vollständigen Regeln für Anteile und Notenbuchstaben sowie is_pass. Das vorgegebene quizapp/report.py verwendet diese Paketregeln. Bearbeite keine der beiden Hilfsdateien in dieser Übung.