0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonにおけるインポート経路の不一致に起因するEnum等価比較の不整合と対策

0
Posted at

はじめに

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つの異なるキーでキャッシュに登録されてしまいます。

  1. from config.constants ...sys.modules['config.constants'] に登録
  2. from constants ...sys.modules['constants'] に登録

これにより、Python内部では config.constants.Statusconstants.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の比較や isinstanceFalse になる。
  • 対策: sys.path を複雑に弄るのを避け、インポートの記述方法をプロジェクト全体で統一(基本はルートからの絶対パス)する。

「コードは何も間違っていないはずなのに、なぜか条件分岐をすり抜ける」というときは、対象のオブジェクトがどのモジュール名(__module__)として読み込まれているかを出力してみるのが、解決への一番の近道です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?