Pythonでプログラムを作成していると、ファイルが増えてくることがあります。
その際、単一ファイルのスクリプトとして管理するのは難しくなります。
さらに、他のプロジェクトで再利用する場合や、チームで開発する場合には、構造的に整理された形式が必要です。
そこで登場するのが「パッケージ化」です。
本記事では、パッケージ化の目的や基本的な構成、分割の理由、そしてセットアップファイル setup.py
の役割について詳しく解説します。
パッケージ化の目的
パッケージ化を行うことで、次のような利点を得られます。
構造化
パッケージ化することで、コードをモジュール単位で整理できます。例えば、データ処理
と出力処理
が混在しているコードを、役割ごとにファイルとして分割できます。これにより、コードの可読性が向上します。
再利用性
一度パッケージ化すれば、同じコードを他のプロジェクトで簡単に再利用できます。特に、頻繁に使用する汎用的な機能は、パッケージ化しておくと便利です。
配布の容易さ
Pythonでは、パッケージ化したコードをPyPI(Python Package Index)などを通じて配布できます。この手順は今回の解説には含まれませんが、パッケージ化することで配布の準備が整います。
メンテナンスの容易化
パッケージ化されたコードはモジュールごとに独立しているため、個々の修正や機能追加が簡単です。
パッケージの基本構成
Pythonのパッケージはディレクトリ構造を持つフォルダであり、通常以下のようなファイルやディレクトリが含まれます。
mypackage/ ├── mymodule/ │ ├── __init__.py │ ├── main.py │ ├── utils.py ├── tests/ │ ├── test_module.py ├── setup.py └── README.md
各ファイル・ディレクトリの役割
mymodule/
パッケージの中心となるディレクトリで、実際のコードが含まれます。このディレクトリ名がパッケージ名になります。__init__.py
パッケージをモジュールとして認識させるためのファイルです。内容は空でも構いません。main.py
プログラムのメイン部分を記述します。今回は、コマンドライン引数を処理する部分が該当します。utils.py
補助的な機能や再利用可能な関数をまとめたファイルです。今回は、スケジュールのデータ読み書きや基本操作を担当します。tests/
テストコードを格納するディレクトリです。テストフレームワーク(例:pytest
)を使って、パッケージ全体の動作確認を行います。setup.py
パッケージのインストール設定を記述するファイルです。パッケージの依存関係やエントリーポイント(後述)もここで指定します。README.md
パッケージの説明を記述するファイルです。主に配布や利用者向けのドキュメントとして使用されます。
コードを分割する理由
なぜmain.py
とutils.py
に分けるのか?
コードを分割する理由は以下の通りです。
- 役割ごとの分離
main.py
はエントリーポイント(実行時に最初に呼び出される部分)としての役割を担います。utils.py
はスケジュールデータの操作や補助的な関数を担当します。 この分離により、コードが見やすくなるだけでなく、テストしやすくなります。
- 再利用性の向上
utils.py
に分けることで、他のプログラムやスクリプトからも簡単に関数を呼び出せます。例えば、スケジュールデータを操作する関数だけを別のツールで利用する場合にも便利です。 - 変更の影響を限定 特定の機能を修正する場合、該当するファイルだけを変更すればよく、他の部分に影響を与えません。
setup.pyの詳細解説
setup.py
は、パッケージの設定やインストール方法を記述する重要なファイルです。以下に典型的な例を示します。
from setuptools import setup, find_packages setup( name="sch", version="0.1.0", packages=find_packages(), install_requires=[], entry_points={ "console_scripts": [ "sch=sch.main:main", ], }, description="A simple schedule management tool", author="Your Name", author_email="your_email@example.com", url="https://example.com/sch", )
各項目の説明
name
パッケージ名を指定します。他のパッケージと重複しない名前を選びます。version
パッケージのバージョン番号を指定します。通常はMAJOR.MINOR.PATCH
の形式を使用します。packages
パッケージとしてインストールするディレクトリを指定します。find_packages()
を使うと、自動的にパッケージを検出します。install_requires
パッケージが依存する他のPythonパッケージを指定します(例:["requests", "numpy"]
)。entry_points
エントリーポイントは、パッケージの実行方法を指定します。"console_scripts"
: コマンドラインから直接プログラムを実行できるようにする設定。- 例:
sch=sch.main:main
sch
: コマンド名。pip install
後に使用可能。sch.main
: パッケージ内のモジュール名。main
: 実行される関数名。
description
パッケージの簡単な説明文です。author
とauthor_email
作成者の名前と連絡先を記載します。
ここまでで、パッケージ化の目的、基本構成、コードの分割理由、そしてsetup.py
の詳細について解説しました。
ここからは、具体的にsch.py
をパッケージ化する手順を説明します。
sch.pyをパッケージ化する手順
ディレクトリ構造を準備
以下のようなディレクトリ構造を作成します。
sch_package/ ├── sch/ │ ├── __init__.py │ ├── main.py │ ├── utils.py ├── tests/ │ ├── test_sch.py ├── setup.py └── README.md
sch/
: 実際のコードを格納するディレクトリ。__init__.py
: パッケージとして認識させるためのファイル(空でOK)。main.py
: スケジュール管理ツールのエントリーポイント。utils.py
: スケジュールの読み書きや操作を行う補助機能。tests/
: テスト用コードを格納するディレクトリ。setup.py
: パッケージのインストール設定。README.md
: パッケージの説明。
sch.py
を分割
sch.py
のコードを以下のように分割します。
main.py
コマンドライン引数を処理し、適切な操作を呼び出す部分を担当します。
import argparse from .utils import add_schedule, list_schedule, delete_schedule def main(): """コマンドライン引数を解析し、処理を分岐します""" parser = argparse.ArgumentParser(description="スケジュール管理プログラム") parser.add_argument("-a", nargs=3, metavar=("DATE", "TIME", "TITLE"), help="スケジュールを追加します") parser.add_argument("-l", metavar="DATE", help="スケジュールを一覧表示します") parser.add_argument("-d", type=int, metavar="ID", help="スケジュールを削除します") args = parser.parse_args() if args.a: add_schedule(args.a[0], args.a[1], args.a[2]) elif args.l: list_schedule(args.l) elif args.d: delete_schedule(args.d) else: parser.print_help()
インポート先のファイルが.utils
となっています。
この形を相対インポートといいます。
例えば、from .utils
という記述は「現在のディレクトリ内にある utils
モジュール」をインポートすることを表します。
絶対インポートという方法もあります。 絶対インポートはプロジェクトのルートディレクトリを基準に、完全なパスを使ってモジュールを指定します。
例:
from sch.utils import function_name
ここでは sch がプロジェクトのルートディレクトリ直下にあるディレクトリです。
ドットはディレクトリの区切子(デリミタ)を表すので、sch.utils
はsch
ディレクトリ下のutils
ファイルの意味になります。
utils.py
スケジュールデータの保存・読み取りと操作を行う補助関数をまとめます。
import json import os DATA_FILE = "schedule.json" def load_data(): """スケジュールデータをロードします""" if os.path.exists(DATA_FILE): with open(DATA_FILE, "r") as f: return json.load(f) return [] def save_data(data): """スケジュールデータを保存します""" with open(DATA_FILE, "w") as f: json.dump(data, f, indent=4) def add_schedule(date, time, title): """スケジュールを追加します""" d = load_data() new_id = d[-1]["id"] + 1 if d else 1 s = {"id": new_id, "date": date, "time": time, "title": title} d.append(s) save_data(d) print(f"スケジュールを追加しました: {date} {time} - {title}") def list_schedule(date): """指定された日付のスケジュールを一覧表示します""" d = load_data() l = [s for s in d if s["date"] == date] if l: print(f"スケジュール一覧 ({date}):") for s in l: print(f"{s['id']}. {s['time']} - {s['title']}") else: print(f"{date}のスケジュールはありません。") def delete_schedule(schedule_id): """指定されたIDのスケジュールを削除します""" d = load_data() d = [s for s in d if s["id"] != schedule_id] save_data(d) print(f"スケジュール(ID: {schedule_id})を削除しました。")
setup.py
を作成
setup.py
を以下のように記述します。
from setuptools import setup, find_packages setup( name="sch", version="0.1.0", packages=find_packages(), install_requires=[], entry_points={ "console_scripts": [ "sch=sch.main:main", ], }, description="A simple schedule management tool", author="Your Name", author_email="your_email@example.com", url="https://example.com/sch", )
author, author_email, urlのところは、実際にはプログラムの作者の氏名、メールアドレス、ウェブサイトアドレスを入れます。
テストコードを配置
tests/test_sch.py
に既存のテストコードを移動します。
ファイルを分割したのでコードの修正が必要です。
import pytest import os from sch import utils TEST_DATA_FILE = "test_schedule.json" # テスト用のデータファイルを一時的に使用するように変更 @pytest.fixture(autouse=True) def setup_and_teardown(): utils.DATA_FILE = TEST_DATA_FILE yield if os.path.exists(TEST_DATA_FILE): os.remove(TEST_DATA_FILE) def test_add_schedule(): """スケジュールの追加をテスト""" utils.add_schedule("2024-12-10", "10:00", "Meeting") utils.add_schedule("2024-12-10", "14:00", "Lunch") data = utils.load_data() assert len(data) == 2 assert data[0]["title"] == "Meeting" assert data[1]["title"] == "Lunch" def test_list_schedule(capsys): """スケジュールの一覧表示をテスト""" utils.add_schedule("2024-12-10", "10:00", "Meeting") utils.add_schedule("2024-12-10", "14:00", "Lunch") utils.list_schedule("2024-12-10") captured = capsys.readouterr() assert "Meeting" in captured.out assert "Lunch" in captured.out def test_delete_schedule(): """スケジュールの削除をテスト""" utils.add_schedule("2024-12-10", "10:00", "Meeting") utils.add_schedule("2024-12-10", "14:00", "Lunch") utils.delete_schedule(1) data = utils.load_data() assert len(data) == 1 assert data[0]["title"] == "Lunch"
パッケージ化したので、以前のように単にpytestとコマンドラインから打ち込むのでは、テストは上手くいきません。 テストはプロジェクトのルートディレクトリから、次のように行います。
PS >py -m pytest tests
テスト後にキャッシュのディレクトリ(.pytest_cache
など)が残りますが、これは削除しても残しておいても構いません。
このような一時ファイルは、後に説明する.gitignore
でバージョン管理の対象から外すことで解決するのが一番良い方法です。
今回はそこまでは触れません。
インストールして実行
パッケージ化が完了したら、次の手順でインストールします。
- ターミナルでルートディレクトリに移動。
- コマンドを実行してパッケージをインストール。
pip install .
引数にドット(カレントディレクトリ)があることに注意してください。
パッケージの作成でもbuild
などのディレクトリが作られます。
これらも.gitignore
で対処するのが良いのですが、説明は別の記事で行い、ここでは触れません。
それらのディレクトリを削除しても良いですが、残しておいても構いません。
sch
コマンドを実行してスケジュール管理ツールを利用できます。
PS > sch -a "2024-12-12" "10:00" "Team Meeting" PS > sch -l "2024-12-12" PS > sch -d 1
これでsch.py
をパッケージ化する手順は完了です。
プロジェクトにおいては、常にパッケージ化を念頭において開発することが望まれます。