コンテンツへスキップ

コード構造と複数のファイル

特に**大規模プロジェクト**で複数のファイルを使用する場合のコード構造について考えてみましょう。

循環インポート

クラス`Hero`は内部的にクラス`Team`を参照しています。

しかし、クラス`Team`もクラス`Hero`を参照しています。

そのため、これらの2つのクラスが別々のファイルにあり、それぞれのファイルでクラスを直接インポートしようとすると、**循環インポート**が発生します。🔄

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

また、このプロジェクトを「**Pythonパッケージ**」(Pythonモジュールの集まり)にするために、空の`__init__.py`ファイルもあります。これにより、`app.py`ファイル/モジュールで**相対インポート**を使用できます。例:

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

これらの相対インポートを使用できるのは、例えば`app.py`ファイル(`app`モジュール)において、Pythonが`__init__.py`ファイルと同じディレクトリにあるため、それが私たちの**Pythonパッケージの一部**であることを認識しているからです。そして、同じディレクトリにあるすべての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`にある**mainブロック**と同じトリックが引き続き機能します。

if __name__ == '__main__':
    main()

そのため、出力は以下のようになります。

fast →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

restart ↻

循環インポートの解決

何らかの理由で、すべてのデータベースモデルを単一のファイルにまとめるという考えが嫌いで、`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.pyTeamを「インポート」することができます。

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

実行時にPythonでエラーが発生しないように、Teamのアノテーションを文字列型: "Team"にする必要があることに注意してください。

チームモデルファイル

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には循環インポートがないため、通常のインポートを使用し、ここで通常どおりクラスを使用できます。

そして、それを実行すると、以前と同じ結果が得られます。

fast →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'

restart ↻

要約

最も簡単なケース(ほとんどの場合)では、すべてのモデルを単一のファイルに保持し、残りのアプリケーション(エンジンの設定を含む)を必要な数のファイルに分割することができます。

そして、すべてのモデルを異なるファイルに分離する必要がある複雑なケースでは、TYPE_CHECKINGを使用してすべてを動作させ、最高のエディタサポートによる最高の開発者エクスペリエンスを実現できます。 ✨