第10回 新しいデータ構造設計(ToDoリスト)

 前回までに、Pythonの基本的な構文や、基本的なデータ構造(リスト・集合・辞書)について一通り学んだ。そろそろサンプルプログラム(トイプログラム)を卒業して、実用的なプログラムに眼を向けてもよいころだ。第11回と第12回で取り組んでもらう課題レポート「スケジュール管理ツール」がそれにあたる。おそらく150~200行規模になるから、あれこれと細部を詰めていかないと、完成は覚束ない。アプリのためのデータ構造設計は、最初にやるべき重要な作業である。今回はまず、ToDoリストのデータ設計を行う。
プログラム設計のヒントとなるテーマも、少しずつ取り上げる。また、これまでの回で扱い損ねた分類不明の項目についても解説してゆく。

この回の内容

ToDoリストの設計

実用プログラム向けのデータ構造設計の例として「ToDoリスト(の項目)」を取り上げよう。今後やるべき予定を記したメモで、課題レポートで扱う「スケジュール」の基礎となるデータだ※1。新たなデータ構造はもちろん、Pythonに備わった整数・文字列・真偽値(Boolean型)といった基本データ型や、リスト・辞書といったデータ構造を組み合わせてつくる。データベースの分野で「テーブル設計(またはスキーマ設計)」と呼ばれる作業とほぼ同じだ。

1 「スケジュール = ToDo項目 + 日付時刻」と考えればよい。

データ構造設計では、1件のToDo項目(やるべき予定)というのは、どのような「フィールド(部品)」から構成され、それぞれはどんなデータ型かを考えてゆく。
たとえば、こんな具合だ。

ずいぶん簡単な構成と思うだろうが、最初は単純にしておき、使っていきながら気付いた不便を解消するように拡張すればよい。

2 ENUM(型)とは、enumerate(数え上げる)の略。整数や真偽値の各値に独自の意味を定義して使うデータを指す。この「重要度」は実態は整数型でも、値の大小に意味はなく、各値は()内で定義された意味を割り当てられただけである。だから、ENUMを使う場合は、必ずこのような定義文を添える必要がある。一方、「実行済」は、「実行済かどうか」という真偽値本来の意味を失っていないので、ENUMではない(定義文は念のために添えた)。

このままではフィールドの羅列だから、1件のToDo項目を変数に代入できるように、リストにまとめる。例を挙げる。


[True, 3, '昼食は焼きビーフン']
[False, 2, '最終課題の解答例を完成']
[False, 1, '春学期の授業開始']
[False, 1, 'めでたく定年を迎えます']

これらの「ToDo項目」をに、実行予定の「日付時刻」をキーとした辞書を作れば、すなわちスケジュール帳ということになるだろう。

書式指定とf文字列

この授業ではいままで、書式指定やf文字列を説明抜きで使っていた。書式指定は、標準出力やファイルに書き込むテキストの形式を細かく制御する構文である。最初のころは、値を出力する変数や文字列を、print()関数の引数に並べて指定したり、


print('Howdy,', name, '!')
文字列連結演算子「+」で1つの文字列につなげたり

print('鈴木さんに' + str(suzuki) + '円返す')
していた。しかし、さまざまなデータ型をもつ多数の要素を、形式を細かく制御しながら表示するには、これらの方法では限界がある。
そこで、多くのプログラミング言語と同様、Pythonには、書式を表す文字列に、表示される要素(変数や式)を埋め込むことで、テキストの表示形式を整える機能がある。これを書式指定つき出力という。
文字列の書式指定には新旧2種類(実は超旧いのも含めて3種類)の方式がある。これからPythonを学ぶみなさんは、ここで解説する最新方式(f文字列)だけを学べばよい。
これには、記号{ }f文字列を用いる。f文字列はPython3.6から導入された最新機能で、正式名を「フォーマット済み文字列リテラル」という。書式指定を従来よりはるかに簡単な構文でできるようにしたものだ。
今までも何回か使ってきたけれど、リスト10-1に改めて例を挙げる。

リスト10-1: f文字列の使い方(fstring.ipynb)

ここで、書式指定の対象となるのは、'料金 = {price}、税込み料金 = {price * 1.1:.2f}'などの部分である。f文字列中の中括弧「{ }」の「穴」に、変数や式の値が埋め込まれる。書式指定がない場合、{ }内の引数はデフォルトの書式で表示されるが、たとえば「:.2f」のような書式指定があれば、それにしたがって表示が整形される※3
これによって、

を明示的に細かく指定できる。

3 { }内の記法は「書式指定ミニ言語」という独立した文法に則っり、かなりのボリュームがあるため、授業では到底説明しきれない。詳細はPython言語リファレンスの「6.1.3. 書式指定文字列の文法」を参考にしてほしい。

クォートと複数行文字列

今までの授業で十分に説明してこなかった別の項目が文字列定数の書き方だ。
Pythonでは文字列定数を、シングルクォート「'」またはダブルクォート「"」で囲む。2種類の引用符を認めるのは、引用符を含む文字列をスマートに表現するためだ。たとえば以下のように。

つまり、シングルクォートを含む文字列はダブルクォートで囲み、逆にダブルクォートを含む文字列はシングルクォートで囲むことで、従来の言語のように美しくないエスケープ文字「\'」「\"」を使わずに「'」「"」自体を表現できる。よく考えられた賢い解決法だ。
さらに「トリプルクォート(''')」もある。なにやら用語が混乱しているが、これは3つのシングルクォート(こちらを強く奨める)(''')または3つのダブルクォート(""")のことだ。以下のように、複数行の文字列を直接表現できる。これもPerl言語などの美しくないヒアドキュメントを洗練したものである。


番号 = int(input('''
番号を入力してください。
1:今日のスケジュール
2:スケジュール一覧
3:スケジュール検索
4:スケジュール登録
5:スケジュール削除
0:プログラムの終了
->'''))

この1文だけでメニュー画面を表示し、ユーザの入力を整数化して変数番号に代入する(エラー処理のことは今は考えない)。2つのトリプルクォートの間は、改行文字も含めた1つの文字列定数である。
トリプルクォートによる複数行文字列は、始めは戸惑うが、慣れると手放せない機能だ。前節で解説したf文字列と組み合わせれば、ワープロの差込み文書に近いことまで実現できる。

データの永続化とpickle

「データの永続化」とは聞き慣れない言葉だが、実用的なプログラムでは避けて通れないテーマである(第8回の授業でディクショナリの「永続化」について少し触れた)。プログラムが実用的と呼べる条件は、処理データがプログラム中にハードコーディング(直接書き込まれていること)されていないとか、処理内容に汎用性があるとかいろいろあるが、アプリ終了後も作業結果が保存され、次回の実行時に引き継がれるというのは、最重要項目だろう。
データのこの性質を永続性(persistency)、データを永続的にすることを永続化という。これに対し、これまでのサンプルプログラムで扱った変数データは、すべて一時的(temporaly)である。アプリ終了とともに内容が失われるからだ。
データに永続性を持たせる手段はいくつかある。

  1. データベース管理システム(DBMS)を用いる。ちなみにPythonにはsqlite3という、便利なリレーショナル・データベース管理システム(RDBMS)が標準で含まれている
  2. テキストファイルとして独自の形式を定義し※4、アプリの開始時と終了時にロード/セーブする
  3. その中間的形態として、Pythonではpickleを使う方法がある

4 市販アプリでよく使われる形式として、TSV(tab separated value)CSV(comma separated value)がある。単純なデータには便利だが、「データ自体にカンマが含まれる場合はどうするか」とか、詰めなくてはならない例外事項もあり、案外と煩雑である。

ところで、永続化されたデータは、アプリが動いていないときにはどこにあるのだろう。 上記のどの方法にせよ、最終的には補助記憶装置上のファイルに保存するしかない(だって、他のどこに記憶できる?)。ここでは、1ほどチャレンジングではなく、2のような煩雑さもない、3のpickleを使う方法を解説する。
pickleはPythonにデフォルトで備わっているシリアライズ※5の仕組みだ。これを使えば、ファイル上のデータ形式を自分で定義しなくてもよくなる。pickleとは、ほら、ピクルス(漬物)のことだ。

5 直列化。オブジェクトのデータをバイト列に変換すること。その逆はデシリアライズ(非直列化)だ。実はPythonではあらゆるデータは「オブジェクト」であり、数値、文字列、リスト、ディクショナリから、ユーザが独自に作ったクラスオブジェクトまで、基本的にはすべてを永続的に格納できる(一部pickle化できないオブジェクトも存在するらしい)。

pickleは、プログラム中のあらゆるデータを1次元のバイト列(文字列ではない)に変換し、拡張子「.pkl」「pickleファイル」に格納する。データの永続化を実現する強力な手段なので、マスターしない手はない。
pickleを用いたデータ永続化の方法を、ディクショナリを例に解説する。はじめにpickleモジュールをインポートしておく。


import pickle

たとえばスケジュール帳のデータが辞書yoteiに格納されているとき、これをpickleにするには、次の処理でよい。


# スケジュールをpickleへ書き出し
with open('schedule.pkl', 'wb') as f:
    pickle.dump(yotei, f)

withは初出だが、使い終えた時にファイルを自動的にcloseしてくれる便利なしかけ。
一方、ファイルからオブジェクトにデータを読み込む(復元)には、以下のようにする※6


# スケジュールをpickleから復元
try:
    f = open('schedule.pkl', 'rb')
    yotei = pickle.load(f)
except:	        # pickleファイルがなくても異常終了しない
    print('初回起動です')
else:
    f.close()

復元が保存に比べやや複雑なのは、初回起動時まだpickleファイルが存在しない場合も異常終了しないようにである。

6 このコードはpickleだけでなく、データをファイルに保存して後の実行時に使う場合の常套句なので、覚えておこう。また、プログラムをテストするときは、pickleファイルを削除してから始めないと予期しない動作になる場合がある。