はじめに
Pythonでリファクタリングや機能追加を行った後、以下のようなコードで条件分岐が意図通りに動かなくなることがあります。
# どちらも同じEnumの同じメンバーを指しているはずなのに...
if status_a == status_b:
print("一致")
else:
print("不一致") # なぜかこちらに到達してしまう!
一見すると怪奇現象のようですが、原因は「インポートの書き方の不一致」にあります。本記事では、この現象の再現コードとその内部メカニズム、そして根本的な対策について解説します。
1. 現象の再現
以下のような、一般的なWebアプリケーションやスクリプトのディレクトリ構成を想定します。
my_project/
│
├── config/
│ └── constants.py # Enumを定義
│
└── main.py # 比較を実行するエントリーポイント
config/constants.py(Enumの定義)
from enum import Enum
class Status(Enum):
ACTIVE = 1
INACTIVE = 2
main.py(問題が発生するコード)
エントリーポイントである main.py で、検索パス(sys.path)の通し方やインポートの書き方が混在すると、この現象が再現します。
import sys
import os
# 再現のため、あえて config フォルダ自体を検索パスに追加
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'config')))
# パターンA: プロジェクトルートからの絶対パスでインポート
from config.constants import Status as StatusA
# パターンB: configフォルダがパスに通っているため、直接インポート
from constants import Status as StatusB
# --- 検証 ---
print(f"StatusA.ACTIVE の値: {StatusA.ACTIVE}")
print(f"StatusB.ACTIVE の値: {StatusB.ACTIVE}")
# 【問題の箇所】同じものに見えるのに False になる!
print(f"等号比較 (StatusA.ACTIVE == StatusB.ACTIVE): {StatusA.ACTIVE == StatusB.ACTIVE}")
# 原因の調査
print(f"StatusA のモジュール名: {StatusA.__module__}")
print(f"StatusB のモジュール名: {StatusB.__module__}")
実行結果
StatusA.ACTIVE の値: Status.ACTIVE
StatusB.ACTIVE の値: Status.ACTIVE
等号比較 (StatusA.ACTIVE == StatusB.ACTIVE): False
StatusA のモジュール名: config.constants
StatusB のモジュール名: constants
画面に出力された値はどちらも Status.ACTIVE です。しかし、等号比較(==)を行うと False と判定されてしまいます。
2. なぜ等号が成立しないのか?
原因は、Pythonがこれらを「中身はそっくりだけど、メモリ上は全く別のクラス」として二重に読み込んでしまっているからです。
Pythonのインポートキャッシュ(sys.modules)の仕組み
Pythonは一度インポートしたモジュールを sys.modules という辞書(キャッシュ)に格納し、2回目以降のインポートを高速化しています。この辞書のキーには「インポート時に指定したモジュール名(パス)」がそのまま使われます。
今回のケースでは、以下のように2つの異なるキーでキャッシュに登録されてしまいます。
-
from config.constants ...→sys.modules['config.constants']に登録 -
from constants ...→sys.modules['constants']に登録
これにより、Python内部では config.constants.Status と constants.Status という、完全に独立した2つのクラス定義が作られます。
Enumの比較仕様
Pythonの Enum は、内部でメンバーの同一性(is 比較)やクラスの一致を確認しています。クラス自体が別物(メモリ上のアドレスが異なる)になってしまっているため、メンバーの値(1)が同じであっても、== の結果は False になります。
これはEnumに限らず、通常のクラスで isinstance() を使った型チェックを行う際にも同様に失敗する原因となります。
3. 根本的な対策
この問題を回避するための鉄則は、「プロジェクト内でのインポート方針を完全に統一すること」です。
対策1:ルートからの絶対パスに統一する(推奨)
もっとも安全で、現代のPythonにおいて標準的なベストプラクティスです。sys.path をコード内で動的に書き換えるのをやめ、常にプロジェクトのルートディレクトリを起点としてインポートを行います。
# 常にもっとも外側のルートから記述する
from config.constants import Status
実行する際は、プロジェクトのルートディレクトリで python main.py を実行するか、環境変数 PYTHONPATH にルートディレクトリを通します。
対策2:相対パスインポートを正しく使う
パッケージ(__init__.py を含むディレクトリ)として構成されている場合は、明示的な相対パスインポートを使用することで、インポート経路のブレを防ぐことができます。
# 同じパッケージ内、あるいは隣接するモジュールから呼ぶ場合
from .constants import Status
まとめ
- 原因: インポートの書き方(経路)が異なると、Pythonは同じファイルを別モジュールとして二重に読み込む。
-
現象: 見た目や値は同じでも、メモリ上は別クラスになるため、Enumの比較や
isinstanceがFalseになる。 -
対策:
sys.pathを複雑に弄るのを避け、インポートの記述方法をプロジェクト全体で統一(基本はルートからの絶対パス)する。
「コードは何も間違っていないはずなのに、なぜか条件分岐をすり抜ける」というときは、対象のオブジェクトがどのモジュール名(__module__)として読み込まれているかを出力してみるのが、解決への一番の近道です。