😏

Pythonの循環インポートを少し理解する

2022/01/24に公開

はじめまして

私はyupix(ゆぴ)と申すものです。基本的にはGitHubなどで適当なプロジェクトを作っています。

はじめに

循環インポートって何?って方のために説明しておくと普段、皆さんが行うモジュールのインポートは以下のような関係と言えます。

ですが、循環インポートではこのようになっています。

このように相互を参照しあっているimportのことを循環インポートと言うと思っています。(間違ってたらごめんなさい)

どのような際に起きるのか

一番多いのはType Hintsをつける際だと思います。次に、ファイルを分割しすぎていて本来同じモジュール内にいるべき関数やクラスが違うモジュールにいる際などにおきます。

どうやって治すのか

Type Hintsの場合は解決策があります。以下のような構造で考えてみましょう。
この方法は python3.7未満では使用できないので注意してください
※2つのファイルはどちらとも同じディレクトリにあります。

user.py
import .action

class User:
    name: str = 'yupix'
    age: str = '123'
action.py
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .user import User

def show_profile(data: User):
    print(data.name, data.age)

さて、このコードでのポイントは3箇所です。

  1. from __future__ import annotations
  2. from typing import TYPE_CHECKING
  3. if TYPE_CHECKING:

一つずつ見ていきます。

1のfrom __future__ import annotations はPythonモジュールで評価されるアノテーションの入力方法を変化させ、注釈の評価を延長します。

2のfrom typing import TYPE_CHECKING はサードパーティーの静的型検査器(mypyなど)がTrueと仮定する特別な定数です。ライブラリ自体を実行するとFalseになります。

3のif TYPE_CHECKING: は2で出てきた定数を使用して、importするかどうかを判断します。これにより、PyCharmやVSCodeなどで型情報が取得できるようになります。

ちなみに、1の from __future__ import annotations はコードを以下のように変更することで省略ができます。

action.py
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .user import User

def show_profile(data: 'User'):
    print(data.name, data.age)

クォーテーションで囲うか囲わないかの違いです。私は囲いたくないので、基本的にimportするようにしています。

別の直し方

importを変更することで治る場合があります。これは__init__.pyにディレクトリのモジュールをインポートすることで名前空間を統一する際に発生することが多い問題だと思います。

以下のようなコードを

__init__.py
from a import *
from b import *
from c import *
d.py
from . import c_hello

c_hello()

このように修正します

d.py
from .c import c_hello

c_hello()

ひとつの名前空間からすべてのモジュールをインポートすることは循環インポートを発生させる危険性を秘めています。そのため、適度に使うことでこの問題は抑えることができるかもしれません。

おわりに

この記事で紹介した方法が全てとは限りません、もっといい方法もあるかもしれません。また、これらの方法で直せない場合はそもそも論クラスや関数をファイル分けしすぎてるのかもしれませんね。

References

https://www.python.org/dev/peps/pep-0563/
https://docs.python.org/ja/3/library/typing.html

Discussion