つくる:保存機能
このレッスンで作るもの — 第 8 章の総仕上げ。記録をファイルに保存し、読み戻して辞書のリストに復元する
save_and_loadを作ります。
つくる — 保存機能
この章では、ファイルの読み込み、書き込み、with、CSV 形式、try-except、例外の種類を扱いました。最後にそれらを 1 本につなぎます。
作るのは save_and_load(path, records) です。渡された辞書のリストを path に保存し、そのファイルを読み戻して、元と同じ形の辞書のリスト を返します。
records = [{"name": "りんご", "price": 120, "category": "食費"}]
save_and_load("/tmp/kakeibo.txt", records)
# [{"name": "りんご", "price": 120, "category": "食費"}]入れたものと同じものが出てくるだけの、地味に見える関数です。しかしこれが成り立つということは、アプリを閉じても記録が失われない ということです。おこづかい帳が初めて「記憶」を持ちます。
保存と読み込みは必ず対で設計します。片方だけ先に作ると、書いた形式が読めないことに後から気づいて作り直しになります。
保存の流れ
書き出す側は前回までの組み合わせです。辞書を CSV の行にして、改行でつないで、with で書きます。
lines = []
for record in records:
lines.append(record["name"] + "," + str(record["price"]) + "," + record["category"])
with open(path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))join を使って ",".join([...]) と書いても同じです。読みやすいほうを選んでください。
復元の流れ
読み戻す側が今回の新しい部分です。行の文字列を、また辞書に戻します。
with open(path, "r", encoding="utf-8") as f:
text = f.read()
loaded = []
for line in text.splitlines():
name, price, category = line.split(",")
loaded.append({"name": name, "price": int(price), "category": category})split(",") で 3 つに分け、第 7 章のタプルアンパックで 3 つの変数に受けています。ここで int(price) を忘れない ことが最大の注意点です。
ファイルから読んだ値はすべて文字列です。price は "120" という文字列のまま入っています。これを int に戻さないと、合計を計算するときに "120" + "80" が "12080" になる、という第 2 章で見た事故が起きます。
書くときに
str、読むときにint。この対称性が崩れると、型がじわじわ混ざって原因の分かりにくいバグになります。
空リストの扱い
記録が 0 件のときは、join の結果が空文字になり、中身が空のファイルができます。読み戻すと read は "" を返し、splitlines は [] を返します。for は 1 度も回らず、戻り値は [] です。
つまり特別な場合分けを書かなくても、空のときは空が返ります。splitlines を選んだ効果がここで効いています。split("\n") を使っていた場合、空文字を分割すると [""] という 1 要素のリストになり、line.split(",") が 1 つしか返せずアンパックで ValueError になります。
手元で動かしてみる
kakeibo.py に次のように書いて保存すると、実際に手元のフォルダにファイルができます。
def save_and_load(path, records):
lines = []
for record in records:
lines.append(record["name"] + "," + str(record["price"]) + "," + record["category"])
with open(path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
with open(path, "r", encoding="utf-8") as f:
text = f.read()
loaded = []
for line in text.splitlines():
name, price, category = line.split(",")
loaded.append({"name": name, "price": int(price), "category": category})
return loaded
print(save_and_load("kakeibo.txt", [{"name": "りんご", "price": 120, "category": "食費"}]))実行したあと、同じフォルダに kakeibo.txt ができているはずです。テキストエディタで開くと りんご,120,食費 と書かれています。プログラムが残した最初の記録です。
よくある間違い
intに戻し忘れる — 金額が"120"のまま返ると、見た目は似ていてもテストは落ちます。辞書に入れる前にint(price)を通します。- キーの順番や名前を変える — 保存の形式は
name、price、categoryの順です。読み戻すときも同じ順で受け取ります。キー名は第 7 章から一貫して英語です。 - 書き込みのあと読まずに
recordsを返す — テストは通ってしまうかもしれませんが、保存できたことの確認になりません。必ずファイル経由で戻します。
with を 1 つにまとめようとして詰まるのも定番です。書きモードのファイルからは読めないので、書きと読みで 2 回に分けます。
やってみよう
関数 save_and_load(path, records) を実装してください。records を "品名,金額,カテゴリ" の行にして path に保存し、そのファイルを読み戻して辞書のリストに復元して返します。
金額は保存時に str、復元時に int を通します。records が空リストのときは空リスト [] が返る形になっていれば正解です。ファイルを開くときは with を使ってください。
要件
- 関数名は save_and_load で、引数は path と records の 2 つ
- 1 件を "品名,金額,カテゴリ" の行にし、改行でつないで path に書き出す
- ファイルを開くときは with を使う
- 書き出したファイルを読み戻し、split でカンマごとに分けて辞書に復元する
- 金額は int に戻し、文字列のままにしない
- records が空リストのときは空リストを返す
入出力例
save_and_load("/tmp/kakeibo_build_1.txt", [{"category":"食費","name":"りんご","price":120},{"category":"食費","name":"みかん","price":80}]) → [{"category":"食費","name":"りんご","price":120},{"category":"食費","name":"みかん","price":80}]
save_and_load("/tmp/kakeibo_build_2.txt", [{"category":"日用品","name":"ノート","price":200}]) → [{"category":"日用品","name":"ノート","price":200}]
save_and_load("/tmp/kakeibo_build_3.txt", []) → []
save_and_load("/tmp/kakeibo_build_4.txt", [{"category":"その他","name":"もらいもの","price":0}]) → [{"category":"その他","name":"もらいもの","price":0}]
save_and_load("/tmp/kakeibo_build_5.txt", [{"category":"交通費","name":"バス","price":220},{"category":"娯楽","name":"映画","price":1800},{"category":"日用品","name":"洗剤","price":380}]) → [{"category":"交通費","name":"バス","price":220},{"category":"娯楽","name":"映画","price":1800},{"category":"日用品","name":"洗剤","price":380}]