程式碼結構與多個檔案¶
讓我們停下來思考一下如何組織程式碼結構,特別是在具有多個檔案的大型專案中。
循環導入¶
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 模組 (或檔案)
app
database
models
我們還有一個空的 __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 的程式碼放在另一個檔案 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 中使用的格式相同:
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
使其全部運作,並且仍然擁有最佳的開發人員體驗和最佳的編輯器支援。 ✨