おもこん

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

Python初級者のお勉強ノート(20)コマンドラインから起動するプログラムの作成

Pythonを使って、コマンドライン上で動作するスケジュール管理プログラムを作成します。 このプログラムでは以下の機能を実装します:

  • スケジュールの追加
  • スケジュールの表示
  • スケジュールの削除

プログラム名はsch.py(schはscheduleの最初の3文字)で、コマンドラインからシンプルに操作できるよう設計します。

スケジュール管理プログラムの設計

今回作成するプログラムの特徴は以下の通りです:

  1. コマンドライン引数で操作:
    • -a: スケジュールの追加
    • -l: スケジュールの一覧表示
    • -d: スケジュールの削除
  2. データの保存と読み込み:
    • JSONファイルを利用します。
  3. シンプルな構造:
    • すべての関数は簡潔で明確な役割を持たせます。

argparseモジュールとは?

argparseモジュールは、Pythonコマンドライン引数を解析するための標準ライブラリです。 このモジュールを使うと、プログラムをコマンドラインから柔軟に操作できるようになります。

argparseの基本的な使い方

1. 引数パーサーを作成する

argparse.ArgumentParserを使って、引数を処理するための「引数パーサー」を作成します。 引数パーサーは、ArgumentParserクラスのインスタンスです。 このパーサーは、コマンドライン引数を解釈し、プログラムで使いやすい形に変換する役割を持っています。

import argparse

parser = argparse.ArgumentParser(description="コマンドライン引数の例")
  • description引数: プログラムの概要を説明します。この説明は、ユーザーが--helpを指定したときに表示されます。 この引数の与え方をキーワード引数といいます。 キーワード引数についてはPython初心者のお勉強ノート(10)関数の基礎を参照してください。

例えば、このコードを含むスクリプトpython script.py --helpで実行すると、以下のようなヘルプメッセージが表示されます:

usage: script.py [-h]

コマンドライン引数の例
2. 引数を定義する

次に、プログラムが受け取る引数を定義します。「引数パーサー」のadd_argumentメソッドを使って、引数の名前や動作を指定します。

parser.add_argument("-a", help="スケジュールを追加する")
  • -a:
    • コマンドライン引数のショートスタイル(短い名前)。
    • 実際には、ユーザーが-aに続けて値を指定します(例: -a value)。
  • help:
    • 引数の説明です。--helpを実行したときに、この引数がどのような目的で使われるのか表示されます。
3. 引数を解析する

最後に、コマンドラインから渡された引数を解析し、その値を取得します。これにはparse_argsメソッドを使用します。

args = parser.parse_args()
  • argsは、ユーザーが指定した引数を保持するオブジェクトです。
  • 属性として各引数にアクセスできます(例: args.a)。

例えば、次のようなコマンドを実行した場合:

> py script.py -a "My Schedule"

プログラム内でargs.aを確認すると、"My Schedule"という文字列が取得できます。

プログラム全体のコード

以下に、sch.pyのコードを示します。 プログラムの説明はコードの後にあります。 このプログラムは、スケジュールの追加、一覧表示、削除の3つの機能を持っています。

import argparse
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)
    else:
        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()
    s = {"id": len(d) + 1, "date": date, "time": time, "title": title}
    d.append(s)
    save_data(d)
    print(f"スケジュールを追加しました: {date} {time} - {title}")

def list_schedule(date):
    """指定された日付のスケジュールを一覧表示します"""
    d = load_data()
    new_id = d[-1]["id"] + 1 if d else 1
    s = {"id": new_id, "date": date, "time": time, "title": title}
    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})を削除しました。")

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()

if __name__ == "__main__":
    main()

プログラムの説明

定数

定数とは常に同じ値を持ち、値を変えない変数のことをいいます。 Pythonは、言語的に定数を持っていないので、変数を定数としても使います。

  • すべて大文字で記述: DATA_FILEのように、大文字の名前が慣習的に定数を意味する。
  • 変更しない前提で利用: 定数として扱う変数は、コード中で変更されないように設計する。

すなわち、プログラマーがこのようなルールを守ることで、定数を使えるようになります。 DATA_FILEは実際は変数なので値を書き換えられますが、それをすると混乱の元になるので、しないようにしてください。

関数の説明コメント

関数の説明がdef文の次の行にあります。 この説明に使われているのは、3個のダブルクォート(トリプルクォート)で囲んだ文字列です。 ダブルクォートの代わりにシングルクォートを使うこともできます。 トリプルクォートの文字列は複数行に渡って記述することができます。

プログラムの説明はコメント文(ハッシュマーク#以後がコメントになる)を使うのが普通ですが、ここで文字列を使っている理由を説明します。

この文字列はDocstringと呼ばれます。 Docstringは、Pythonのコードに関する説明やドキュメントを記述するための文字列です。 クラス、関数、メソッド、モジュールなどの冒頭に記述され、コードの動作や使い方を明確に伝えるために利用されます。 Docstringにはトリプルクォートを使います。

  • この文字列は、関数などの__doc__属性に保存される
  • Docstringはhelp関数で参照することができる
  • ドキュメント生成ツール(例: Sphinx)で利用される

ハッシュマークのコメント文を使うと、そのコメントは実行時に無視されてしまうので、関数などの説明には必ず文字列を使ってください。 ドキュメント生成ツールについては、今後別の記事で説明する予定です。

load_data()save_data()

  • 役割: スケジュールデータを読み込みまたは保存します。ファイル形式はJSONです。
  • ポイント:

add_schedule(date, time, title)

  • 役割: 新しいスケジュールをデータに追加します。
  • ポイント:
    • ローカル変数dを使ってスケジュールリストを管理。
    • sは新規スケジュールを表す辞書。
    • 新しいidの作成部分は次の「条件式」の項を参照。

条件式

Pythonには式の中に条件を含む「条件式」があります。 C言語三項演算子(A ? B : C)と同等のものですが、構文が違います。

<Trueのときの値> if <条件> else <Falseのときの値>

この構文は次のように動作します。

  • <条件>を評価。
  • 条件がTrueの場合、<Trueのときの値>が選ばれ、その値が返される。
  • 条件がFalseの場合、<Falseのときの値>が選ばれ、その値が返される。

if文との違い

  • 通常のif文は、条件に応じてスイート内のコードを実行する。文であって式ではないので、値を返すことはしない。
  • 条件式は値を返す。同様のことはif文を使ってもできるが、複数行にわたり長くなる。条件式を使えば1行でシンプルに記述できる。

プログラム中の条件式の動作

new_id = d[-1]["id"] + 1 if d else 1

スケジュールのリストdを評価する。 このとき、リストが空ならFalse、そうでなければTrueと判定します(このあと詳しく説明)。 したがって

  • リストが空でなければ、リストの最後のidに1を加えたものをnew_idに代入する。
  • リストが空ならば、1をnew_idに代入する。

Pythonのif文などの式評価では、bool型オブジェクト(TrueとFalse)以外も使えます。 以下の値がFalse(偽)として評価されます(「Falsy」とも呼ばれる)。

  • None
  • 数値の0(0, 0.0, 0jなど)
  • 空のシーケンス([], "", ())
  • 空のマッピング({})
  • False

それ以外の値はすべてTrue(真)として評価されます(「Truthy」とも呼ばれる)。

list_schedule(date)

  • 役割: 指定された日付のスケジュールを表示します。
  • ポイント:
    • リスト内包表記(後述)を使用して、条件に一致するスケジュールを抽出。
    • 結果が空の場合、適切なメッセージを表示します。

リスト内法表記

Pythonリスト内包表記は、簡潔で読みやすい方法でリストを作成するための構文です。 1行でリストの生成ができます。

リスト内包表記の基本構文

以下が基本的な構文です:

[<式> for <変数> in <イテラブル> if <条件>]  # 条件部分は省略可能
  • <式>: リストに追加される要素を表します。
  • <変数>: forループで使われる変数です。
  • <イテラブル>: リストや文字列、範囲などの反復可能オブジェクトです。
  • if <条件>: 任意の条件を指定できます。この条件を満たす場合のみリストに要素が追加されます。

sch.pyでは、リスト内包表記を使ってスケジュールを抽出しています。

l = [s for s in d if s["date"] == date]
  • s for s in d:
    • リストdの各要素(s)を走査します。
  • if s["date"] == date:
    • スケジュールの"date"が指定されたdateと一致する場合のみ、sをリストに追加します。
  • 結果:
    • 条件に一致するスケジュールのリストが作成されます。

同じ処理を通常のforループで書くこともできますが、1行では書けません。

l = []
for s in d:
    if s["date"] == date:
        l.append(s)

リスト内包表記を使うことで、これを1行で簡潔に記述できます。

リスト内包表記の利点

  1. 簡潔で読みやすい:
    • 1行でリストを生成でき、コードが短くなります。
  2. 高速:
    • 内部で最適化されているため、通常のforループより高速に動作する場合があります。
  3. 柔軟性:
    • 条件式や複雑な処理を組み込むことができます。

リスト内包表記は糖衣構文として実装されています。 ここで、糖衣構文について簡単に説明します。

糖衣構文とは、プログラムの処理内容を簡潔に書けるようにするための表記方法です。 糖衣構文は新しい文法を追加したわけではなく、実際にはそれを同等な通常の構文に変換してPythonが解釈する仕組みです。 例えば、sch.pyの糖衣構文は、forループに書き直してPythonに解釈、実行されます。

リスト内包表記

l = [s for s in d if s["date"] == date]

展開されたforループ

l = []
for s in d:
    if s["date"] == date:
        l.append(s)

糖衣構文の特徴

    1. プログラムの本質には影響を与えない:
    2. 糖衣構文は、単にコードを簡潔に記述できる手段であり、実行される処理内容は通常の構文と同じです。
    1. 新しい機能ではなく既存の仕組みの簡略化:
    2. 糖衣構文は新しいPythonの文法ではなく、既存の機能を別の形で書きやすくしたものです。
    1. 例: リスト内包表記:
    2. Pythonがリスト内包表記をサポートするのは、forループとif文を簡潔に記述できるようにするためです。

なお、糖衣構文はPythonに組み込まれているもので、プログラマーが糖衣構文を作ることはできません。

delete_schedule(schedule_id)

  • 役割: 指定されたIDのスケジュールを削除します。
  • ポイント:
    • 削除後のデータリストを更新する際にもリスト内包表記を活用。

main()

  • 役割: コマンドライン引数を解析し、適切な関数を呼び出します。
  • ポイント:
    • -a, -l, -dといったオプションを指定し、それに応じた処理を実行。argparseモジュールの補足説明は次の項で行います。
    • 引数が不足している場合はヘルプメッセージを表示します。

argparseモジュールについての補足説明

1. -a 引数
parser.add_argument("-a", nargs=3, metavar=("DATE", "TIME", "TITLE"), help="スケジュールを追加します")

オプションの意味

  • -a:

    • ショートオプション。コマンドライン-aを指定することで、この引数を利用できます。
  • nargs=3:

    • この引数は3つの値(引数)を必要とします。
    • たとえば、python sch.py -a 2023-12-10 14:00 "Meeting"とすると、2023-12-1014:00Meetingがそれぞれ別々に渡されます。
  • metavar=("DATE", "TIME", "TITLE"):

    • ヘルプメッセージに表示されるプレースホルダー名です。
    • コマンドライン引数として何を入力するべきかを示します。
    • 例えば、python sch.py --helpで次のように表示されます:
-a DATE TIME TITLE  スケジュールを追加します
  • help="スケジュールを追加します":
    • ヘルプメッセージに表示されるこの引数の説明です。
2. -l 引数
parser.add_argument("-l", metavar="DATE", help="スケジュールを一覧表示します")

オプションの意味

  • -l:

    • ショートオプション。コマンドライン-lを指定することで、この引数を利用できます。
  • metavar="DATE":

    • ヘルプメッセージで、この引数に入力するべき値をDATEとして表示します。
    • 例えば、python sch.py --helpで次のように表示されます:
-l DATE  スケジュールを一覧表示します
  • help="スケジュールを一覧表示します":
    • ヘルプメッセージに表示されるこの引数の説明です。
3. -d 引数
parser.add_argument("-d", type=int, metavar="ID", help="スケジュールを削除します")

オプションの意味

  • -d:

    • ショートオプション。コマンドライン-dを指定することで、この引数を利用できます。
  • type=int:

    • この引数に指定される値を整数型に変換します。
    • ユーザーが文字列や浮動小数点数などの無効な値を入力すると、自動的にエラーになります。
  • metavar="ID":

    • ヘルプメッセージで、この引数に入力するべき値をIDとして表示します。
    • 例えば、python sch.py --helpで次のように表示されます:
-d ID  スケジュールを削除します
  • help="スケジュールを削除します":
    • ヘルプメッセージに表示されるこの引数の説明です。
まとめ
  1. nargs:

    • 必要な引数の個数を指定します(例: nargs=3は3つの値が必要)。
  2. metavar:

    • ヘルプメッセージで引数の説明をわかりやすくするためのプレースホルダーを設定します。
  3. type:

    • 引数の型を指定し、不適切な型の入力を自動的にチェックします。
  4. help:

    • 引数の用途を説明する文字列で、--help実行時に表示されます。

__name____main__

if __name__ == "__main__":

この構文は、スクリプトが直接実行されたときだけ、その下のコードを実行するための仕組みです。

  • __name__:
    • Pythonが各モジュールに自動的に割り当てる特殊な変数。
    • スクリプトが直接実行された場合、__name__"__main__"となります。
    • 他のモジュールからインポートされた場合、__name__はモジュール名になります。

単にコマンドラインから実行するだけなら、この部分は必要ありません。 直接main関数を呼び出すだけで十分です。 しかし、次のような場合にはこのif文が必要になります。

  • そのプログラムが2つの可能性(1ー直接実行される、2ーモジュールとして他のプログラムに使われる)があるとき
  • そのプログラムのテストで、テスト・フレームワークがこのプログラムをインポートする可能性があるとき
  • ドキュメント生成ツール(例: Sphinx)を使うとき。Sphinxはそのプログラムをインポートするため

今回は、次回以降でテストを行うので、このif文をつけておきました。

実行例

  1. スケジュールの追加
> py sch.py -a 2023-12-10 14:00 "ミーティング"
スケジュールを追加しました: 2023-12-10 14:00 - ミーティング
  1. スケジュールの一覧表示
> py sch.py -l 2023-12-10
スケジュール一覧 (2023-12-10):
1. 14:00 - ミーティング
  1. スケジュールの削除
> py sch.py -d 1
スケジュール(ID: 1)を削除しました。

まとめ

本記事では、Pythonを使ったスケジュール管理プログラムを作成しました。

  1. argparseモジュールを使ったコマンドライン引数の処理
  2. JSONファイルを使ったデータの永続化
  3. 関数を分割して明確に役割を定義する設計
  4. 定数、Docstring、リスト内包表記

このプログラムは、コマンドラインツールの作成方法の例となります。 しかし、これだけで十分なわけではなく、テスト、パッケージ化なども必要です。 それらは、次回以降の記事で取り上げたいと思います。