第12回 課題レポート2:スケジュール管理ツール②

卒業課題「スケジュール管理ツール」に鋭意取り組み中のこと思う。今回は、いくつかのヒントと、これまで扱わなかった、やや規模の大きいプログラムの構成法について解説している。大規模なプログラムでは、小さなサンプルプログラムで問題にならなかった、あるいは目立たなかった事柄にも注意を払わなくてはならないからだ。

この回の内容

メニューシステムと画面遷移

ユーザとのインタラクションに応じて内部状態が遷移するアプリでは、フローチャートの他に、画面遷移図を描くと設計が捗る。ユーザーの入力と、それによる表示画面の推移を表した図である。図12-1に例を示す。

図12-1: 「スケジュール管理ツール」の画面遷移図(例)

フローチャートと似ているが、それぞれの箱の中身は、処理内容ではなく、表示される画面を示している(角丸長方形は、その違いを強調するため)。画面間をつなぐ矢印は、別の画面への移動を、矢印のラベル(0~5)は、ユーザが入力したコマンド番号を示す。
Colabノートブックや、コマンドプロンプト(第1回の【参考】項目を参照)のようなCUI環境で、ユーザから繰り返しコマンドを受け取って処理を行うシステムは、この図に示すような「メニューシステム」として構成するのが一般的だ。昭和からあるテレビのセットトップボックスや、平成に爆発的に普及したiモード端末で使われていた方式だ。
アプリを実行すると、図の左端にあるメインメニューが表示される。ユーザにコマンドを選択してもらうプロンプト文であり、第10回にヒントを示した通り、複数行文字列('''~''')を用いて表示できる。
入力されたコマンド番号によって、それぞれ別の画面(図の右側各段)に遷移し、各コマンドの実行が始まる。単に情報を表示するだけの場合もあれば、ユーザに(日付・時刻・登録内容など)別の情報入力を求める場合もある。日付や時刻など、ディクショナリyoteiのキーと形式が一致しているかのチェックが必要なこともある(第13回(Aコース)の正規表現の演習課題)。終了コマンド(0)が選ばれるまで、アプリの実行は続く※1

1 「スケジュール帳の保存や復元はどこ?」なんて言ってる人は、まだフローチャートと画面遷移図を混同している。

タイムゾーンについて

日時の取得や表示は、datetimeモジュールを使えば簡単に行える。たとえば以下に示すコード


from datetime import datetime
# 日付・時刻を文字列として取得し表示
t = datetime.now()
print(t.strftime('%Y-%m-%d %H:%M'))
は、このような表示を出す(この形式がスケジュール帳のキーとなる)。

2025-03-24 09:49
ローカルなPC環境なら、このままで問題ない。一般にそのPCが置かれた現地時刻(たとえば日本/東京時刻(JST))が返るからだ。
しかし、ColabはUTC時刻(協定世界時)を返すので、それと「+09:00」の時差があるJSTを得るには、「タイムゾーン」を設定しなければならず、この手順は初級者にはハードルが高い。日付と時刻を返す関数として定義したので、以下をコピペしてよい。

from datetime import date, datetime
from pytz import timezone

# 日付・時刻を文字列として取得
def 日時(t = ''):       # 「現在時刻」以外にも使えるように
    # Google ColabのタイムゾーンはUTC時刻なので、日本時刻に設定
    tz = timezone('Asia/Tokyo')
    if not t:           # 引数省略時は「現在時刻」
        t = datetime.now().astimezone(tz)
    tstr = t.strftime('%Y-%m-%d %H:%M')
    return tstr         # 例:2025-03-24 09:49

#    日付を文字列として取得
def 日付(t = ''):       # 「今日」以外にも使えるように
    tstr = 日時(t)      # 引数省略時は「現在時刻」→「今日」
    d = tstr[0:10]
    return d

アプリの全体構成

画面遷移図を元に、アプリの全体構成を考えよう。「終了」コマンドが選ばれるまでは、メインメニューが繰り返し表示されるから、全体は1つの無限ループ(while True:)になる。
ここで注意がいるのは、コードセルの分割だ。これまでのサンプルプログラムでもコードセルをいくつかに分割していたが、これはどこで分けてもよいのではなく、たとえば条件分岐や繰り返し構文や関数定義(def)のコードブロックの途中、つまりインデントされた部分では分けられない。しかしそれでは、長いプログラム全体を、単一のコードセルに収めなくてはならず、全体の構造が見通しにくくなるし、テストも困難になってしまう。
そこでお奨めしたいのは、各コマンドを独立した関数とし、最後のコードセルにメインメニューの表示ループを置いて、ユーザが入力したコマンド番号に応じてそれらの関数を呼び分けることだ。この機構を「コマンドディスパッチャ」という。
構成のイメージがつかめただろうか。

スケジュール情報(yotei)を各関数で共有するには

ここで検討しなければならないことがある。すべてのスケジュールはディクショナリyoteiに格納され、メインメニュー表示の周辺や、各コマンド関数でその内容を読み書きする(つまり変数yoteiを共有している)。その場合、本来なら関数定義の外で(グローバル変数として)yoteiを定義する。


yotei = dict()        # 本番用

yotei = {             # テスト用
    '2025-03-24 12:00':[True, 3, '昼食は焼きビーフン'],
    '2025-03-24 17:00':[False, 2, '最終課題の解答例を完成'],
    '2025-04-15 09:00':[False, 1, '春学期の授業開始'],
    '2030-03-30 00:00':[False, 1, 'めでたく定年を迎えます']
}
その場合、各関数の中(ローカル名前空間)からyoteiを参照するには、

def 登録():
    global yotei
        :
のように、関数定義の最初にglobal宣言を置く必要がある。それだけならよいのだが、このルールには細則がくっついていて、しかもそれがすこぶる分かりにくい。 このあたりは履修者への説明に困ってしまうので、必要性に関わらず、すべての関数定義にglobal宣言を置いたりして、お茶を濁しているのだが、Colab環境だとさらに、書かれた位置ではなくコードセルの実行順に変数定義がなされるため、それでも参照エラーが出る場合がある※2

2 個人的には、Colab環境ではグローバル変数を使わないことを奨めたい。とはいえ、関数定義外で宣言された変数はどれもグローバル変数になってしまうが。少なくとも、関数定義内で(グローバル変数と知りつつ)同名の変数を使うべきではない。グローバル変数はどこからでも参照できて便利だが、逆にいえばプログラム全体に予期しない影響を与え、原因が究明しにくいバグにつながる(そのため、「構造化プログラミング」では避けるべき事柄とされている。言語仕様として、この問題に終止符を打ったのが「オブジェクオ指向プログラミング」のパラダイムと、その具体的な機構であるクラスとオブジェクトだが、この「プログラミング入門」では扱う余地がない。

そこで別の解決策として、メインメニューから呼ばれる各コマンド関数に、以下のように引数yoteiを渡すことにすれば、参照にまつわる複雑さを回避できる。

def 登録(yotei):
        :
各コマンド関数の引数インタフェースがこれで統一されていれば(yoteiに触らない関数にも渡すことになるが)、コマンドディスパッチャもすっきり記述できる。

# コマンドディスパッチャ(意味を考えよう)
[終了, 今日, 一覧, 検索, 登録, 削除][番号](yotei)
ここまでの節で説明した内容をColabノートブックとしてまとめたのが、リスト12-1に示すテンプレートである。授業フォルダに置くので、自力での開発に限界を感じている人は利用してほしい。

リスト12-1: 「スケジュール管理ツール」のテンプレート

「pass」について

テンプレート中の関数定義で使われた初出の「pass」文は、空の実行ブロックを表す。ご存じのようにPythonはコードブロックをインデントで表すので、「空のコードブロック(インデント)」を表現できない。そこで、他言語でついぞ見かけないこのキーワードが要るのだ。他言語の多く(C系やjavaなど)では、空のコードブロックは{ }と書ける。
これからはある程度長いプログラムを、テストしながら開発することも多くなる。その場合、未実装部分にpassを置けば、少なくとも文法的には実行可能なプログラムなので、テストのために実行できるようになる。
また、開発途上でなくても、オブジェクト指向プログラミングで使われるクラスメソッドでは、上位クラスからのメソッドの継承をクラス階層の途中で抑止するために、空のメソッドを定義する場合があり、そこでもpassが使われる。