Zum Inhalt springen

Code-Struktur und mehrere Dateien

Lassen Sie uns kurz innehalten, um darüber nachzudenken, wie man den Code strukturiert, insbesondere in großen Projekten mit mehreren Dateien.

Zirkuläre Importe

Die Klasse Hero hat intern einen Verweis auf die Klasse Team.

Aber die Klasse Team hat auch einen Verweis auf die Klasse Hero.

Wenn diese beiden Klassen also in separaten Dateien wären und Sie versuchen würden, die Klassen direkt in den Dateien des jeweils anderen zu importieren, würde dies zu einem zirkulären Import führen. 🔄

Und Python kann dies nicht verarbeiten und wird einen Fehler ausgeben. 🚨

Aber wir wollen diese zirkuläre Referenz tatsächlich *meinen*, denn in unserem Code könnten wir verrückte Dinge tun, wie

hero.team.heroes[0].team.heroes[1].team.heroes[2].name

Und diese zirkuläre Referenz ist es, was wir mit diesen Beziehungsattributen ausdrücken, dass

  • Ein Held kann ein Team haben
    • Dieses Team kann eine Liste von Helden haben
      • Jeder dieser Helden kann ein Team haben
        • ...und so weiter.

Schauen wir uns verschiedene Strategien an, um den Code zu strukturieren und dies zu berücksichtigen.

Einzelnes Modul für Modelle

Dies ist die einfachste Art. ✨

In dieser Lösung verwenden wir immer noch mehrere Dateien, für die models, für die database und für die app.

Und wir könnten alle weiteren benötigten Dateien haben.

Aber in diesem ersten Fall würden alle Modelle in einem einzigen Datei leben.

Die Dateistruktur des Projekts könnte sein

.
├── project
    ├── __init__.py
    ├── app.py
    ├── database.py
    └── models.py

Wir haben 3 Python-Module (oder Dateien)

  • app
  • database
  • models

Und wir haben auch eine leere __init__.py-Datei, um dieses Projekt zu einem "Python-Paket" (einer Sammlung von Python-Modulen) zu machen. Auf diese Weise können wir relative Importe in der Datei/dem Modul app.py verwenden, wie

from .models import Hero, Team
from .database import engine

Wir können diese relativen Importe verwenden, weil Python zum Beispiel in der Datei app.py (dem app-Modul) weiß, dass es Teil unseres Python-Pakets ist, da es sich im selben Verzeichnis wie die Datei __init__.py befindet. Und alle Python-Dateien im selben Verzeichnis gehören ebenfalls zum selben Python-Paket.

Models-Datei

Sie könnten alle Datenbankmodelle in einem einzigen Python-Modul (einer einzigen Python-Datei) unterbringen, z. B. models.py

from typing import List, Optional

from sqlmodel import Field, Relationship, SQLModel


class Team(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    headquarters: str

    heroes: List["Hero"] = Relationship(back_populates="team")


class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str
    age: Optional[int] = Field(default=None, index=True)

    team_id: Optional[int] = Field(default=None, foreign_key="team.id")
    team: Optional[Team] = Relationship(back_populates="heroes")

Auf diese Weise müssten Sie sich nicht mit zirkulären Importen für andere Modelle auseinandersetzen.

Und dann könnten Sie die Modelle aus dieser Datei/diesem Modul in jeder anderen Datei/jedem anderen Modul Ihrer Anwendung importieren.

Datenbank-Datei

Dann könnten Sie den Code zur Erstellung der Engine und die Funktion zur Erstellung aller Tabellen (wenn Sie keine Migrationen verwenden) in eine andere Datei database.py schreiben

from sqlmodel import SQLModel, create_engine

sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)

Diese Datei würde auch von Ihrem Anwendungscode importiert, um die gemeinsam genutzte Engine zu verwenden und die Funktion create_db_and_tables() abzurufen und aufzurufen.

Anwendungs-Datei

Schließlich könnten Sie den Code zur Erstellung der App in eine andere Datei app.py schreiben

from sqlmodel import Session

from .database import create_db_and_tables, engine
from .models import Hero, Team


def create_heroes():
    with Session(engine) as session:
        team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar")

        hero_deadpond = Hero(
            name="Deadpond", secret_name="Dive Wilson", team=team_z_force
        )
        session.add(hero_deadpond)
        session.commit()

        session.refresh(hero_deadpond)

        print("Created hero:", hero_deadpond)
        print("Hero's team:", hero_deadpond.team)


def main():
    create_db_and_tables()
    create_heroes()


if __name__ == "__main__":
    main()

Hier importieren wir die Modelle, die Engine und die Funktion zur Erstellung aller Tabellen und können sie dann intern verwenden.

Reihenfolge ist wichtig

Erinnern Sie sich, dass die Reihenfolge wichtig ist, wenn SQLModel.metadata.create_all() aufgerufen wird?

Der Punkt dieses Abschnitts in der Dokumentation ist, dass Sie das Modul mit den Modellen vor dem Aufruf von SQLModel.metadata.create_all() importieren müssen.

Wir tun das hier, wir importieren die Modelle in app.py und danach erstellen wir die Datenbank und die Tabellen, also sind wir auf der sicheren Seite und alles funktioniert korrekt. 👌

Ausführung über die Kommandozeile

Da dies nun ein größeres Projekt mit einem Python-Paket und nicht nur einer einzelnen Python-Datei ist, können wir es nicht mehr aufrufen, indem wir einfach einen einzelnen Dateinamen übergeben, wie wir es zuvor mit

$ python app.py

Jetzt müssen wir Python mitteilen, dass wir ein *Modul* ausführen möchten, das Teil eines Pakets ist

$ python -m project.app

-m bedeutet, dass Python ein *Modul* aufrufen soll. Und das nächste, was wir übergeben, ist eine Zeichenkette mit project.app, das ist das gleiche Format, das wir in einem Import verwenden würden

import project.app

Dann führt Python dieses Modul *innerhalb* dieses Pakets aus, und da Python es direkt ausführt, funktioniert der gleiche Trick mit dem main-Block, den wir in app.py haben, immer noch

if __name__ == '__main__':
    main()

Die Ausgabe wäre also

$ python -m project.app

Created hero: id=1 secret_name='Dive Wilson' team_id=1 name='Deadpond' age=None
Hero's team: name='Z-Force' headquarters='Sister Margaret's Bar' id=1

Zirkuläre Importe funktionieren lassen

Nehmen wir an, aus irgendeinem Grund gefällt Ihnen die Idee nicht, alle Datenbankmodelle in einer einzigen Datei zu haben, und Sie möchten wirklich separate Dateien haben, eine Datei hero_model.py und eine Datei team_model.py.

Das können Sie auch tun. 😎 Es gibt ein paar Dinge zu beachten. 🤓

Warnung

Das ist etwas fortgeschrittener.

Wenn die obige Lösung für Sie bereits funktioniert hat, ist das vielleicht genug und Sie können im nächsten Kapitel weitermachen. 🤓

Nehmen wir an, die Dateistruktur ist jetzt

.
├── project
    ├── __init__.py
    ├── app.py
    ├── database.py
    ├── hero_model.py
    └── team_model.py

Zirkuläre Importe und Typ-Annotationen

Das Problem bei zirkulären Importen ist, dass Python sie zur Laufzeit nicht auflösen kann.

Aber bei der Verwendung von Python Typ-Annotationen ist es sehr üblich, den Typ einiger Variablen mit Klassen deklarieren zu müssen, die aus anderen Dateien importiert werden.

Und die Dateien mit diesen Klassen müssen möglicherweise auch mehr Dinge aus den ersten Dateien importieren.

Und dies führt zu den gleichen zirkulären Importen, die in Python zur Laufzeit nicht unterstützt werden.

Typ-Annotationen und Laufzeit

Aber diese Typ-Annotationen, die wir deklarieren möchten, werden zur Laufzeit nicht benötigt.

Tatsächlich haben wir, wie Sie sich erinnern, list["Hero"] verwendet, mit einem "Hero" in einem String?

Für Python ist das zur Laufzeit nur ein String.

Wenn wir also die benötigten Typ-Annotationen mithilfe der String-Versionen hinzufügen könnten, hätte Python kein Problem.

Aber wenn wir nur Strings in die Typ-Annotationen einfügen würden, ohne etwas zu importieren, wüsste der Editor nicht, was wir meinen, und könnte uns nicht mit Autovervollständigung und Inline-Fehlern helfen.

Wenn es also eine Möglichkeit gäbe, einige Dinge zu "importieren", die nur beim Bearbeiten des Codes als "importiert" wirken, aber nicht zur Laufzeit, würde das das Problem lösen... Und das gibt es! Genau das. 🎉

Nur beim Bearbeiten importieren mit TYPE_CHECKING

Um das zu lösen, gibt es einen besonderen Trick mit einer speziellen Variable TYPE_CHECKING im Modul typing.

Sie hat den Wert True für Editoren und Tools, die den Code mit den Typ-Annotationen analysieren.

Aber wenn Python den Code ausführt, ist ihr Wert False.

Wir können sie also in einem if-Block verwenden und Dinge innerhalb des if-Blocks importieren. Und sie werden nur für Editoren "importiert", aber nicht zur Laufzeit.

Hero-Model-Datei

Mit diesem Trick von TYPE_CHECKING können wir Team in hero_model.py "importieren"

from typing import TYPE_CHECKING, Optional

from sqlmodel import Field, Relationship, SQLModel

if TYPE_CHECKING:
    from .team_model import Team


class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str
    age: Optional[int] = Field(default=None, index=True)

    team_id: Optional[int] = Field(default=None, foreign_key="team.id")
    team: Optional["Team"] = Relationship(back_populates="heroes")

Beachten Sie, dass wir die Annotation von Team jetzt als String schreiben müssen: "Team", damit Python zur Laufzeit keine Fehler hat.

Team-Model-Datei

Wir verwenden den gleichen Trick in der Datei team_model.py

from typing import TYPE_CHECKING, List, Optional

from sqlmodel import Field, Relationship, SQLModel

if TYPE_CHECKING:
    from .hero_model import Hero


class Team(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    headquarters: str

    heroes: List["Hero"] = Relationship(back_populates="team")

Jetzt erhalten wir Editor-Unterstützung, Autovervollständigung, Inline-Fehler und SQLModel funktioniert weiterhin. 🎉

App-Datei

Nun, nur der Vollständigkeit halber, die Datei app.py würde die Modelle aus beiden Modulen importieren

from sqlmodel import Session

from .database import create_db_and_tables, engine
from .hero_model import Hero
from .team_model import Team


def create_heroes():
    with Session(engine) as session:
        team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar")

        hero_deadpond = Hero(
            name="Deadpond", secret_name="Dive Wilson", team=team_z_force
        )
        session.add(hero_deadpond)
        session.commit()

        session.refresh(hero_deadpond)

        print("Created hero:", hero_deadpond)
        print("Hero's team:", hero_deadpond.team)


def main():
    create_db_and_tables()
    create_heroes()


if __name__ == "__main__":
    main()

Und natürlich sind alle Tricks mit TYPE_CHECKING und Typ-Annotationen in Strings nur in den Dateien mit zirkulären Importen erforderlich.

Da es keine zirkulären Importe mit app.py gibt, können wir hier einfach normale Importe verwenden und die Klassen wie gewohnt nutzen.

Und das Ausführen davon liefert das gleiche Ergebnis wie zuvor

$ python -m project.app

Created hero: id=1 age=None name='Deadpond' secret_name='Dive Wilson' team_id=1
Hero's team: id=1 name='Z-Force' headquarters='Sister Margaret's Bar'

Zusammenfassung

Für die einfachsten Fälle (für die meisten Fälle) können Sie alle Modelle in einer einzigen Datei belassen und den Rest der Anwendung (einschließlich der Einrichtung der Engine) in so vielen Dateien strukturieren, wie Sie möchten.

Und für die komplexen Fälle, die wirklich erfordern, alle Modelle in verschiedene Dateien aufzuteilen, können Sie TYPE_CHECKING verwenden, um alles zum Laufen zu bringen und trotzdem die beste Entwicklungserfahrung mit der besten Editor-Unterstützung zu erhalten. ✨