おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

Python初級者のお勉強ノート(22)パッケージの作成

Pythonでプログラムを作成していると、ファイルが増えてくることがあります。 その際、単一ファイルのスクリプトとして管理するのは難しくなります。 さらに、他のプロジェクトで再利用する場合や、チームで開発する場合には、構造的に整理された形式が必要です。 そこで登場するのが「パッケージ化」です。 本記事では、パッケージ化の目的や基本的な構成、分割の理由、そしてセットアップファイル setup.py の役割について詳しく解説します。

パッケージ化の目的

パッケージ化を行うことで、次のような利点を得られます。

構造化

パッケージ化することで、コードをモジュール単位で整理できます。例えば、データ処理出力処理が混在しているコードを、役割ごとにファイルとして分割できます。これにより、コードの可読性が向上します。

再利用性

一度パッケージ化すれば、同じコードを他のプロジェクトで簡単に再利用できます。特に、頻繁に使用する汎用的な機能は、パッケージ化しておくと便利です。

配布の容易さ

Pythonでは、パッケージ化したコードをPyPIPython Package Index)などを通じて配布できます。この手順は今回の解説には含まれませんが、パッケージ化することで配布の準備が整います。

メンテナンスの容易化

パッケージ化されたコードはモジュールごとに独立しているため、個々の修正や機能追加が簡単です。

パッケージの基本構成

Pythonのパッケージはディレクトリ構造を持つフォルダであり、通常以下のようなファイルやディレクトリが含まれます。

mypackage/
├── mymodule/
│   ├── __init__.py
│   ├── main.py
│   ├── utils.py
├── tests/
│   ├── test_module.py
├── setup.py
└── README.md

各ファイル・ディレクトリの役割

  1. mymodule/ パッケージの中心となるディレクトリで、実際のコードが含まれます。このディレクトリ名がパッケージ名になります。
  2. __init__.py パッケージをモジュールとして認識させるためのファイルです。内容は空でも構いません。
  3. main.py プログラムのメイン部分を記述します。今回は、コマンドライン引数を処理する部分が該当します。
  4. utils.py 補助的な機能や再利用可能な関数をまとめたファイルです。今回は、スケジュールのデータ読み書きや基本操作を担当します。
  5. tests/ テストコードを格納するディレクトリです。テストフレームワーク(例: pytest)を使って、パッケージ全体の動作確認を行います。
  6. setup.py パッケージのインストール設定を記述するファイルです。パッケージの依存関係やエントリーポイント(後述)もここで指定します。
  7. README.md パッケージの説明を記述するファイルです。主に配布や利用者向けのドキュメントとして使用されます。

コードを分割する理由

なぜmain.pyutils.pyに分けるのか?

コードを分割する理由は以下の通りです。

  1. 役割ごとの分離
    • main.py はエントリーポイント(実行時に最初に呼び出される部分)としての役割を担います。
    • utils.py はスケジュールデータの操作や補助的な関数を担当します。 この分離により、コードが見やすくなるだけでなく、テストしやすくなります。
  2. 再利用性の向上 utils.py に分けることで、他のプログラムやスクリプトからも簡単に関数を呼び出せます。例えば、スケジュールデータを操作する関数だけを別のツールで利用する場合にも便利です。
  3. 変更の影響を限定 特定の機能を修正する場合、該当するファイルだけを変更すればよく、他の部分に影響を与えません。

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",
)

各項目の説明

  1. name パッケージ名を指定します。他のパッケージと重複しない名前を選びます。
  2. version パッケージのバージョン番号を指定します。通常は MAJOR.MINOR.PATCH の形式を使用します。
  3. packages パッケージとしてインストールするディレクトリを指定します。find_packages() を使うと、自動的にパッケージを検出します。
  4. install_requires パッケージが依存する他のPythonパッケージを指定します(例: ["requests", "numpy"])。
  5. entry_points エントリーポイントは、パッケージの実行方法を指定します。
    • "console_scripts": コマンドラインから直接プログラムを実行できるようにする設定。
    • 例: sch=sch.main:main
      • sch: コマンド名。pip install 後に使用可能。
      • sch.main: パッケージ内のモジュール名。
      • main: 実行される関数名。
  6. description パッケージの簡単な説明文です。
  7. authorauthor_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となっています。 この形を相対インポートといいます。

  • . は現在のパッケージを意味します(カレントディレクトリ)。
  • .. は親ディレクトリ(1階層上)を意味します。
  • ... はさらにその親ディレクトリ(2階層上)を意味します。

例えば、from .utils という記述は「現在のディレクトリ内にある utils モジュール」をインポートすることを表します。

絶対インポートという方法もあります。 絶対インポートはプロジェクトのルートディレクトリを基準に、完全なパスを使ってモジュールを指定します。

例:

from sch.utils import function_name

ここでは sch がプロジェクトのルートディレクトリ直下にあるディレクトリです。 ドットはディレクトリの区切子(デリミタ)を表すので、sch.utilsschディレクトリ下の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でバージョン管理の対象から外すことで解決するのが一番良い方法です。 今回はそこまでは触れません。

インストールして実行

パッケージ化が完了したら、次の手順でインストールします。

  1. ターミナルでルートディレクトリに移動。
  2. コマンドを実行してパッケージをインストール。
pip install .

引数にドット(カレントディレクトリ)があることに注意してください。 パッケージの作成でもbuildなどのディレクトリが作られます。 これらも.gitignoreで対処するのが良いのですが、説明は別の記事で行い、ここでは触れません。 それらのディレクトリを削除しても良いですが、残しておいても構いません。

  1. schコマンドを実行してスケジュール管理ツールを利用できます。
PS > sch -a "2024-12-12" "10:00" "Team Meeting"
PS > sch -l "2024-12-12"
PS > sch -d 1

これでsch.pyをパッケージ化する手順は完了です。 プロジェクトにおいては、常にパッケージ化を念頭において開発することが望まれます。