代码结构和多个文件¶
让我们花点时间思考如何构建代码,特别是在包含多个文件的大型项目中。
循环导入¶
Hero 类内部引用了 Team 类。
但 Team 类也引用了 Hero 类。
因此,如果这两个类在单独的文件中,并且您尝试直接在彼此的文件中导入这些类,这将导致循环导入。🔄
Python 将无法处理并会抛出错误。🚨
但我们实际上希望表达这种循环引用,因为在我们的代码中,我们可以做一些疯狂的事情,比如
hero.team.heroes[0].team.heroes[1].team.heroes[2].name
这种循环引用就是我们用这些关系属性表达的,即
- 一个英雄可以有一个团队
- 该团队可以有一个英雄列表
- 这些英雄中的每一个都可以有一个团队
- ...等等。
- 这些英雄中的每一个都可以有一个团队
- 该团队可以有一个英雄列表
让我们看看针对这种情况,构建代码的不同策略。
模型的单一模块¶
这是最简单的方法。✨
在这个解决方案中,我们仍然使用多个文件,用于 models、database 和 app。
我们还可以有任何其他必要的文件。
但在这种情况下,所有模型都将存在于一个单一文件中。
项目的文件结构可以是
.
├── project
├── __init__.py
├── app.py
├── database.py
└── models.py
我们有 3 个 Python 模块(或文件)
appdatabasemodels
我们还有一个空的 __init__.py 文件,使该项目成为一个“Python 包”(Python 模块的集合)。这样我们就可以在 app.py 文件/模块中使用相对导入,例如
from .models import Hero, Team
from .database import engine
我们可以使用这些相对导入,因为,例如,在文件 app.py(app 模块)中,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 来使所有工作正常进行,并且仍然拥有最佳的开发体验和最佳的编辑器支持。✨