跳到内容

代码结构和多个文件

让我们花点时间思考如何构建代码,特别是在包含多个文件的大型项目中。

循环导入

Hero 类内部引用了 Team 类。

Team 类也引用了 Hero 类。

因此,如果这两个类在单独的文件中,并且您尝试直接在彼此的文件中导入这些类,这将导致循环导入。🔄

Python 将无法处理并会抛出错误。🚨

但我们实际上希望表达这种循环引用,因为在我们的代码中,我们可以做一些疯狂的事情,比如

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

这种循环引用就是我们用这些关系属性表达的,即

  • 一个英雄可以有一个团队
    • 该团队可以有一个英雄列表
      • 这些英雄中的每一个都可以有一个团队
        • ...等等。

让我们看看针对这种情况,构建代码的不同策略。

模型的单一模块

这是最简单的方法。✨

在这个解决方案中,我们仍然使用多个文件,用于 modelsdatabaseapp

我们还可以有任何其他必要的文件

但在这种情况下,所有模型都将存在于一个单一文件中。

项目的文件结构可以是

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

我们有 3 个 Python 模块(或文件)

  • app
  • database
  • models

我们还有一个空的 __init__.py 文件,使该项目成为一个“Python 包”(Python 模块的集合)。这样我们就可以在 app.py 文件/模块中使用相对导入,例如

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

我们可以使用这些相对导入,因为,例如,在文件 app.pyapp 模块)中,Python 知道它是我们 Python 包的一部分,因为它与 __init__.py 文件在同一个目录中。同一个目录中的所有 Python 文件也都是同一个 Python 包的一部分。

模型文件

您可以将所有数据库模型放在一个 Python 模块(一个 Python 文件)中,例如 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")

这样,您就不必处理其他模型的循环导入。

然后,您可以在应用程序中的任何其他文件/模块中从该文件/模块导入模型。

数据库文件

然后,您可以将创建引擎的代码以及创建所有表的函数(如果您不使用迁移)放在另一个文件 database.py

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)

该文件也将被您的应用程序代码导入,以使用共享的引擎并获取和调用 create_db_and_tables() 函数。

应用文件

最后,您可以将创建应用程序的代码放在另一个文件 app.py

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()

在这里,我们导入模型、引擎以及创建所有表的函数,然后我们可以在内部使用它们。

顺序很重要

还记得调用 SQLModel.metadata.create_all()顺序很重要 吗?

文档中该部分的重点是,您必须在调用 SQLModel.metadata.create_all() 之前导入包含模型的模块。

我们在这里是这样做的,我们在 app.py 中导入模型,之后我们创建数据库和表,所以我们没问题,一切都正常工作。👌

在命令行中运行

因为现在这是一个包含Python 包的较大项目,而不是单个 Python 文件,所以我们不能像以前那样只传递一个文件名来调用它

$ python app.py

现在我们必须告诉 Python 我们希望它执行一个属于某个包的模块

$ python -m project.app

-m 用于告诉 Python 调用一个模块。我们传递的下一件事是字符串 project.app,这与我们在导入中使用的格式相同

import project.app

然后 Python 将执行该包内部的该模块,并且由于 Python 直接执行它,我们在 app.py 中的主块的相同技巧仍然有效

if __name__ == '__main__':
    main()

因此,输出将是

$ 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

使循环导入生效

假设由于某种原因,您不喜欢将所有数据库模型放在一个文件中,并且您确实希望拥有单独的文件,一个 hero_model.py 文件和一个 team_model.py 文件。

您也可以这样做。😎 有几点需要注意。🤓

警告

这有点高级。

如果上面的解决方案对您有效,那可能就足够了,您可以继续下一章。🤓

让我们假设现在的文件结构是

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

循环导入和类型注解

循环导入的问题是 Python 无法在 运行时 解决它们。

但是当使用 Python 类型注解时,通常需要用从其他文件导入的类来声明某些变量的类型。

而包含这些类的文件可能还需要从第一个文件导入更多内容。

这最终要求与 Python 在运行时不支持的相同循环导入

类型注解和运行时

但是我们想要声明的这些类型注解运行时是不需要的。

事实上,还记得我们使用 list["Hero"],其中 "Hero" 在一个字符串中吗?

对于 Python,在运行时,这只是一个字符串

因此,如果我们可以使用字符串版本添加我们需要的类型注解,Python 就不会有问题。

但是,如果我们只在类型注解中放置字符串,而不导入任何东西,编辑器将不知道我们是什么意思,并且无法帮助我们进行自动补全行内错误

所以,如果有一种方法可以“导入”一些东西,这些东西只在编辑代码时才作为“导入”而存在,而不是在 运行时,那就可以解决问题了……而且它确实存在!正是如此。🎉

仅在编辑时使用 TYPE_CHECKING 导入

为了解决这个问题,有一个特殊的技巧,使用 typing 模块中一个特殊的 变量 TYPE_CHECKING

对于使用类型注解分析代码的编辑器和工具,它的值为 True

但是当 Python 执行时,它的值为 False

所以,我们可以在一个 if 块中使用它,并在 if 块内导入东西。它们将只为编辑器“导入”,而不是在运行时导入。

英雄模型文件

使用 TYPE_CHECKING 的技巧,我们可以在 hero_model.py 中“导入” Team

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")

请记住,现在我们必须Team 的注解作为字符串:"Team",这样 Python 在运行时才不会出现错误。

团队模型文件

我们在 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")

现在我们获得了编辑器支持、自动补全、行内错误,并且 SQLModel 仍然可以正常工作。🎉

应用文件

现在,为了完整性,app.py 文件将从两个模块导入模型

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()

当然,所有关于 TYPE_CHECKING 和字符串中类型注解的技巧仅在包含循环导入的文件中才需要

由于 app.py 没有循环导入,我们可以像平常一样使用普通导入和类。

运行它会得到与以前相同的结果

$ 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'

回顾

对于最简单的情况(大多数情况),您可以将所有模型保存在一个文件中,并将应用程序的其余部分(包括设置引擎)组织成任意数量的文件。

对于真正需要将所有模型拆分到不同文件中的复杂情况,您可以使用 TYPE_CHECKING 来使所有工作正常进行,并且仍然拥有最佳的开发体验和最佳的编辑器支持。✨