こんにちは、かつコーチです。
「さっきまで動いていたのに、ファイルを1つ分割しただけでImportErrorが出るようになった」
そんな経験はないでしょうか。
原因を探ると、AがBをimportし、BもAをimportしている循環importだった、というのはよくある話です。
この記事では、循環importがなぜ起きるのか、実際のエラーメッセージを見ながら再現し、遅延importと設計変更による解決策を解説します。
読み終える頃には、循環importに遭遇しても慌てず対処できるようになります。
循環importが起きる状況を再現する
モデルとサービスが互いを参照するケース
ECサイトのような構成を想定します。
models.pyにUserクラス、services.pyに注文処理の関数を置きます。
# models.py
from services import calculate_discount
class User:
def __init__(self, name, points):
self.name = name
self.points = points
def get_discount(self, price):
return calculate_discount(self, price)
# services.py
from models import User
def calculate_discount(user: User, price: int) -> int:
if user.points > 100:
return int(price * 0.9)
return price
Userがcalculate_discountを使いたくてservicesをimportし、services側も型ヒントのためにUserをimportしています。
一見自然な設計ですが、この時点で循環importが仕込まれています。
実際に発生するImportErrorのメッセージ
models.pyを直接実行してみます。
python models.py
Traceback (most recent call last):
File "models.py", line 1, in <module>
from services import calculate_discount
File "/path/to/services.py", line 1, in <module>
from models import User
ImportError: cannot import name 'User' from partially initialized module 'models' (most likely due to a circular import) (/path/to/models.py)
筆者も初めてこのエラーを見たとき、「models.pyにUserは確かに定義してあるのに、なぜ見つからないんだ」と数十分悩みました。
原因は名前の消し忘れではなく、import処理の途中で自分自身に戻ってきてしまうことにあります。
なぜ「途中で戻ってくる」と失敗するのか
Pythonはモジュールを読み込むとき、ファイルの中身を上から順に実行します。
models.pyを実行すると、1行目のfrom services import calculate_discountでservices.pyの読み込みが始まります。
services.pyの1行目はfrom models import Userですが、この時点のmodelsモジュールはまだUserクラスの定義に到達する前の状態です。
そのため「modelsモジュールは存在するが、Userという名前はまだない」というエラーになります。
partially initialized module(初期化途中のモジュール)という表現が、まさにこの状態を指しています。
よくあるつまずきポイント・エラー対処
相互import自体をやめられないときの遅延import
すぐに設計を見直す時間がない場合、遅延import(関数の中でimportする方法)で応急処置ができます。
❌ Before(ファイル先頭でimportして循環が起きる)
# services.py
from models import User
def calculate_discount(user: User, price: int) -> int:
if user.points > 100:
return int(price * 0.9)
return price
✅ After(関数の中でimportする)
# services.py
def calculate_discount(user, price: int) -> int:
from models import User # 関数内でimportすることで実行タイミングを遅らせる
if not isinstance(user, User):
raise TypeError("userはUserインスタンスである必要があります")
if user.points > 100:
return int(price * 0.9)
return price
関数の中に書いたimportは、その関数が呼び出された時点で初めて実行されます。
モジュール読み込み時にはまだ実行されないため、循環にぶつかりません。
ただし型ヒントとして使えなくなる副作用があるので、あくまで応急処置と考えてください。
型ヒントを維持したい場合は、if TYPE_CHECKING:ブロックを使う方法もあります。
# services.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from models import User
def calculate_discount(user: "User", price: int) -> int:
if user.points > 100:
return int(price * 0.9)
return price
TYPE_CHECKINGは実行時にはFalseなので、実際のimportは発生しません。
型チェッカー(mypyなど)を使うときだけUserが解決される仕組みです。
根本解決は「共通部分を第3のモジュールに切り出す」設計変更
遅延importは対症療法にすぎません。
根本的には、AとBが互いを直接参照する構造そのものを見直す必要があります。
有効な手段は、両者が依存する共通部分を第3のモジュールに切り出すことです。
# discount.py(新設:割引ロジックだけを独立させる)
def calculate_discount_amount(points: int, price: int) -> int:
if points > 100:
return int(price * 0.9)
return price
# models.py
from discount import calculate_discount_amount
class User:
def __init__(self, name, points):
self.name = name
self.points = points
def get_discount(self, price):
return calculate_discount_amount(self.points, price)
# services.py
from models import User
def notify_discount(user: User, price: int) -> None:
discount_price = user.get_discount(price)
print(f"{user.name}様の割引後価格は{discount_price}円です")
discount.pyはどちらのモジュールにも依存していないため、循環の起点になりません。
依存の方向が「models.py → discount.py」「services.py → models.py」と一方向に整理され、循環そのものが構造的に発生しなくなります。
筆者の経験では、循環importが起きるコードはたいてい「1つのファイルに責務を詰め込みすぎている」サインでもあります。
エラーの応急処置だけで終わらせず、設計の見直しにつなげることをおすすめします。
まとめ
この記事のポイント
- 循環importは、AがBを、BがAをimportし合う構造で発生する
- エラーメッセージの
partially initialized moduleは、読み込み途中で自分自身に戻ってきたことを示す - 遅延import(関数内import)や
TYPE_CHECKINGは応急処置として有効 - 根本解決には、共通ロジックを第3のモジュールに切り出す設計変更が必要
- 循環importの多発は、責務が分離できていない設計上のサインでもある
次に読むべき記事
- dataclassでシンプルにクラスを定義する
- Pythonで簡単なCLIツールを作ってみる(argparse活用)
- モジュールとインポートの基本
タグ: Python, 上級者向け, エラー解決