Pythonにおけるモジュールとパッケージの管理手法

モジュールの基本構成と認識

Pythonのランタイムは、モジュールを3つのカテゴリに分類します。標準の.pyファイルで記述されたソースモジュール、C拡張などとしてコンパイルされた共有ライブラリ、そしてインタープリタに組み込まれているビルトインモジュールです。いずれの場合も、importキーワードを用いて読み込む点は共通しています。

ユーザー定義モジュールの作成は単純です。任意のPythonコードを記述し、拡張子.pyで保存するだけで完了します。追加のキーワードや特殊な構文は不要です。例として、data_handler.pyというファイルを作成し、以下のように記述します。

DEFAULT_CONF = "system_v2"
records = [10, 25, 40, 55]

def execute_task(target):
    print(f"Processing: {target}")

class DataProcessor:
    pass

このファイルが環境の探索パス上に配置されていれば、以下のようにアクセス可能です。

>>> import data_handler
>>> print(data_handler.DEFAULT_CONF)
system_v2
>>> data_handler.records
[10, 25, 40, 55]
>>> data_handler.execute_task("batch_01")
Processing: batch_01
>>> instance = data_handler.DataProcessor()
>>> instance
<data_handler.DataProcessor object at 0x7f...>

モジュールの探索パスの仕組み

import data_handler が実行されると、インタープリタは定義されたディレクトリリストを順に参照します。検索対象は以下の通りです。

  • 実行中のスクリプトが存在するディレクトリ(対話モードの場合はカレントディレクトリ)
  • 環境変数 PYTHONPATH に設定されたディレクトリ群
  • インタープリタのインストールディレクトリ配下にある標準ライブラリパス

現在の探索リストは sys.path から確認できます。

>>> import sys
>>> sys.path
['', '/usr/local/lib/python3.10/site-packages', ...]

自作モジュールを確実に認識させるには、上記のいずれかのディレクトリに配置するか、実行時にパスを追加します。

>>> sys.path.append('/home/user/custom_libs')
>>> import data_handler  # 正常に読み込まれる

読み込み後に module.__file__ を参照すれば、実際に使用されたファイルの物理パスを確認できます。

インポート構文のバリエーション

モジュール内の名前空間を呼び出し元に反映する方法はいくつか存在します。

基本形式
import module_name はモジュールオブジェクト自体をローカル環境にバインドします。内部の変数や関数には module_name.attr でアクセスします。これにより、名前の衝突を防ぎ、コードの可読性が保たれます。

>>> import data_handler as dh
>>> dh.execute_task("test")
Processing: test

特定名前の直接抽出
from module_name import attr を使用すると、指定した属性が直接ローカル名前空間に展開されます。既存の変数名と重複する場合は上書きされるため注意が必要です。

>>> from data_handler import records, DataProcessor
>>> records
[10, 25, 40, 55]
>>> p = DataProcessor()

別名を付けることで衝突を回避できます。

>>> from data_handler import DEFAULT_CONF as cfg, DataProcessor as Processor

全属性の一括読み込み
from module_name import * は名前空間を汚染する可能性があるため、本番コードでは推奨されません。ただし、プロトタイピング時やコンソール操作時は便利です。関数スコープ内では SyntaxError となる点に留意してください。
存在しないモジュールや属性を読み込もうとした場合は ImportError が発生します。これを try...except で捕捉することで、フォールバック処理を実現できます。

try:
    import nonexistent_lib
except ImportError:
    print("Library unavailable, using fallback.")

名前空間の確認とスクリプト実行の切り替え

組み込み関数 dir() を使用すると、現在のスコープや指定モジュールに定義されている名前の一覧を取得できます。

>>> dir(data_handler)
['DEFAULT_CONF', 'DataProcessor', 'execute_task', 'records', '__name__', ...]

.py ファイルはモジュールとして読み込まれるだけでなく、コマンドラインから直接実行することも可能です。この挙動を制御するのが __name__ 属性です。モジュールとしてインポートされた場合、__name__ にはモジュール名が設定されます。一方、スクリプトとして直接実行された場合は __main__ が設定されます。これを利用することで、同じファイルでテストコードや実行エントリポイントを定義できます。

# calculator.py
def compute_power(base, exponent):
    return base ** exponent

if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1:
        b = int(sys.argv[1])
        e = int(sys.argv[2])
        print(f"Result: {compute_power(b, e)}")

インポート時には何もおきませんが、python calculator.py 2 10 のように実行すると Result: 1024 と出力されます。

動的なモジュールの再読み込み

インタープリタはパフォーマンス維持のため、同じモジュールを複数回インポートしても初回のみを読み込みます。開発中にコードを修正して反映させたい場合は、importlib.reload() を使用します。

>>> import data_handler
>>> import importlib
>>> # コード編集後
>>> importlib.reload(data_handler)
<module 'data_handler' from '...'>

パッケージ構造と初期化処理

関連するモジュールが増加した際、ディレクトリ単位で名前空間を階層化するのがパッケージです。任意のディレクトリに .py ファイルを配置するだけでパッケージとして機能します(Python 3.3以降)。

例として toolbox/ ディレクトリに core.pyhelpers.py を配置したとします。

>>> import toolbox.core
>>> toolbox.core.run_task()

または from toolbox import helpers のように部分的に読み込めます。import toolbox だけではサブモジュールは自動では読み込まれないため、必要に応じて明示的なインポートが必要です。

パッケージディレクトリ直下に __init__.py を配置すると、そのパッケージが読み込まれる際に自動実行されます。これはパッケージレベルのグローバル変数の初期化や、サブモジュールの自動公開に適しています。

# toolbox/__init__.py
VERSION = "1.0.0"
from . import core
from . import helpers

これにより、import toolbox 時に corehelpers が同時に利用可能になります。

import * の制御と __all__

パッケージに対して from pkg import * を実行した場合、デフォルトでは何もインポートされません。これは意図しない名前空間の汚染を防ぐ設計です。特定のサブモジュールのみを公開対象にしたい場合は、__init__.py 内で __all__ リストを定義します。

# toolbox/__init__.py
__all__ = ["core", "helpers"]

これで from toolbox import * を実行すると、リストされたモジュールのみがローカル環境に展開されます。モジュール単位でも同様の制御が可能であり、インポート対象を明示的に制限できます。

サブパッケージと相対インポート

パッケージは任意の深さでネストできます。

toolbox/
├── sub_a/
│   └── processor.py
└── sub_b/
    └── analyzer.py

ドット表記で階層を指定してアクセスします。

>>> import toolbox.sub_a.processor

同じ階層にある別のサブパッケージの機能を利用する際、絶対インポートも相対インポートも使用できます。相対インポートでは . が現在のパッケージ、.. が親パッケージを指します。

# toolbox/sub_b/analyzer.py
from ..sub_a.processor import analyze_data

これにより、パッケージ内部の循環参照や階層依存関係を明確に管理することが可能になります。

タグ: Python python-modules python-packages import-statement namespace-management

6月7日 16:54 投稿