🍣

Pythonのパッケージ管理の基礎をマスターする

に公開

ここでは、Pythonのパッケージ管理について解説する。
初心者にとってはとっつきにくいテーマだが、学べばコード理解が格段に進むので、わかりやすく解説したい。

モジュール、パッケージ、ライブラリの違いを理解しよう

基本的な用語の整理

まずは、よく混同されがちな3つの用語を明確にしておこう。

  • モジュール: 1つのPythonファイル(例:math_utils.py
  • パッケージ: __init__.pyを含むディレクトリで複数のモジュールをまとめたもの
  • ライブラリ: 複数のパッケージやモジュールの集合(例:pandas、numpy)

パッケージの構造例

実際のパッケージは以下のような構造になっている:

mypackage/          ← パッケージ(ディレクトリ)
├── __init__.py     ← 「これはパッケージです」という目印
├── math_utils.py   ← モジュール(.pyファイル)
├── string_utils.py ← モジュール(.pyファイル)
└── database/       ← サブパッケージ
    ├── __init__.py ← サブパッケージの目印
    └── connection.py

この構造により、関連する機能を論理的にグループ化でき、メンテナンスしやすいコードベースが構築できる。
このときPythonを入れておくフォルダ(dataとかlogとかのフォルダでないフォルダ)には、とりあえず__init__.pyを配置するようにしよう。

実行ファイルと読み込みファイルの区別

Pythonでのファイルは、実行するファイルと、実行ファイルからimportなどで読み込まれるファイル(読み込んだファイルからさらにimportで読み込まれるファイルも含む)にわかれている。
これを最初におさえておくとパッケージ管理について理解が進みやすい。

__name__による実行コンテキストの判別

__name__はPython開発者が最初に「何これ?」と思う暗号の1つだろう。
これには、そのPythonファイルが直接実行ファイルかimportされたファイルかの情報が付与される。

もしそのファイルが直接実行した場合には、__name____main__という値が入り、importされたファイルならば、__name__にはそのファイルのルート(実行Pythonファイル)からのパスが入る。

print(f"__name__ = {__name__}")

if __name__ == "__main__": # 直接実行の場合、__name__は__main__になる
    print("🎯 実行ファイル: プロセスの制御権を持つ")
    # ここにメインの実行ロジックを記述
else: # 直接実行しない場合、__name__は例えば「mypackage.file」のような値になる。
    print("📚 読み込みファイル: ライブラリとして機能")

この仕組みにより、同じファイルをライブラリとしても実行ファイルとしても使用が可能になる。

Pythonがパッケージを見つける仕組み

sys.pathの役割

Pythonがモジュールやパッケージを探す際は、sys.pathに含まれるディレクトリを順番に検索する。

import sys
print(sys.path)
# 出力例:
# [
#   '/home/user/project',              # 実行ファイルのディレクトリ
#   '/usr/lib/python3.9',              # 標準ライブラリ
#   '/usr/lib/python3.9/site-packages' # インストール済みパッケージ
# ]

sys.pathを見れば、Pythonがimportする対象が存在するフォルダ一覧を確認できるのだ。

sys.modulesによるキャッシュ機能

一度読み込まれたモジュールはsys.modulesにキャッシュされる。これにより、同じモジュールの再インポート時に実行時間が短縮される。

import math
print('math' in sys.modules)  # True

importの後に、sys.modulesの中身を見てみよう。すると、importしたライブラリを一覧で確認できるはずだ。

init.pyファイルの重要性

パッケージの証明書として機能

__init__.pyファイルには、そのディレクトリがパッケージであることをPythonに伝える役割がある。

パッケージがインポートされた際に最初に実行されるため、以下のような設定が可能だ:

# mypackage/__init__.py
from .database import Database
from .utils import helper_function

# 公開するものを明示(これは「from mypackage imoprt *」としたときに読み込まれるもの一覧を表す)
__all__ = ['Database', 'helper_function']

これにより、外部からは以下のようにアクセスできる:

from mypackage import Database, helper_function

このとき、「from . import」部分で、mypackage内の__init__.pyが呼び出され、そのあと、databaseファイルとutilsファイルをそれぞれ呼び出して定義していく流れになっている。

インポート方法の違い

import vs from import

Pythonには、2つの主要なインポート方法がある:

# 方法1: import
import math
result = math.sqrt(16)  # 属性アクセスが必要

# 方法2: from import(推奨)
from math import sqrt
result = sqrt(16)  # 直接関数を呼び出し

from importの方が、関数呼び出し時の記述が簡潔になるため、一般的に推奨される。

ちなみにfrom importの場合は、import対象となる部分(上の例だとsqrt部分)だけを呼び出しているように思われがちだが、そうではなく、mathを定義したファイルは全て実行されている。

絶対インポートと相対インポート

パッケージ内でのインポートには2つの方法がある:
実行ファイルからのパスである絶対インポートか、配置されているファイルからパスを表す相対インポートかだ。

# 絶対インポート(プロジェクトルート(実行ファイル)から)
import mypackage.utils
from mypackage.subpackage import advanced

# 相対インポート(「.」は現在位置と同じディレクトリ、「..」は現在位置の一つ上のディレクトリを表す)
from . import utils           # 同ディレクトリ
from .subpackage import advanced  # サブディレクトリ
from .. import config         # 親ディレクトリ

相対インポートは、パッケージ内の構造変更に強く(構造が変わってもファイル間の位置関係が変わらなければimportできる)、パッケージ内から別のファイルをimportする際には相対インポートを用いることが多い。

実践的なパッケージ構成例

実際のプロジェクトでは、以下のような構成が推奨される。
この例では、main.pyが実行用のファイル、それ以外はmain.pyからimportされる。あるいは、importされた対象ファイルからimportするファイルとなっている。

project/
├── main.py
├── config.py
└── myapp/
    ├── __init__.py # このフォルダの構造を定義する
    ├── core.py
    ├── models.py
    ├── utils/
    │   ├── __init__.py # このフォルダの構造を定義する
    │   ├── validators.py
    │   └── helpers.py
    └── database/
        ├── __init__.py # このフォルダの構造を定義する
        ├── models.py
        └── connection.py

実装例

# main.py
from config import get_config
from myapp.database import User
from myapp.utils import validate_user
from myapp import processs_data

def main():
    app_config = get_config()
    print(f'デバッグモード: {app_config["debug"]}')
    test_data = User(name='Taro', email='taro@mail.com')
    if validate_user(test_data): # チェック完了
        result = processs_data(app_config, test_data)
        print(f'処理成功: {result}')

    
if __name__ == '__main__':
    main()
# myapp/core.py
from .database import Database # 相対インポート
from .utils import log_message # 相対インポート

def processs_data(app_config, user):
    with Database(
        app_config['database_url'], app_config['api_key']
    ) as connection:
        # DBにデータを登録
        result = {
            'processed': True,
            'user': user
        }
        log_message(f'データ処理完了: {result}')
        return result

まとめ

Pythonのパッケージ管理は、以下の要素で構成されている:

  1. モジュール: 単一のPythonファイル
  2. パッケージ: __init__.pyを含むディレクトリ構造
  3. インポート機能: sys.pathsys.modulesによる効率的な読み込み
  4. 実行コンテキスト: __name__による柔軟な使い分け

これらの仕組みを理解することで、保守性の高い、再利用可能なPythonコードが書けるようになる。

特に大規模なプロジェクトでは、適切なパッケージ構成が開発効率を大きく左右するため、早い段階でこれらの概念をマスターしておくことが重要だ。

パッケージ管理をうまく活用して、より効率的なPython開発を実現していこう。
より詳細なメカニズム理解は、Python Distilled
を参考にされると良いと思う。

弊社では、Pythonをはじめとしてエンジニア向けの講座をたくさん用意している。各講座10時間を超えるような長い物でその分野のことをみっちりと学べるようになっている。
ご活用していただけると幸いだ。

コース一覧

Discussion