跳到內容

程式碼結構與多個檔案

讓我們停下來思考一下如何組織程式碼結構,特別是在具有多個檔案的大型專案中。

循環導入

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.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 使其全部運作,並且仍然擁有最佳的開發人員體驗和最佳的編輯器支援。 ✨