月間レポート機能
このレッスンで作るもの — ひと月分の記録から、合計・カテゴリ別・最高額・予算の残りをまとめて返す
monthly_reportを作ります。第 6 章のsummarizeと第 7 章のtotal_by_categoryを 1 つに束ねる回です。
月間レポート機能
前回の run_kakeibo で、追加と削除ができるようになりました。ただ、記録が溜まっただけでは何も分かりません。「今月は何にいくら使ったのか」を一目で分かる形にするのが今回の仕事です。
レポートに載せる 4 つの数字
家計簿を見返すときに知りたいのは、だいたい下記の 4 つです。
total— 今月の合計金額by_category— カテゴリごとの合計を入れた辞書top— いちばん高かった支出の品名remaining— 予算からの残り
このうち total は第 6 章の summarize で、by_category は第 7 章の total_by_category ですでに作りました。remaining の考え方は第 1 章の remaining と第 3 章の check_budget で扱っています。新しいのは top だけです。
完成間近のアプリに機能を足すとき、ゼロから考えることはめったにありません。持っている部品を組み替えて、足りない 1 つだけを新しく書きます。
4 つを 1 回のループで求める
total のために 1 周、by_category のためにもう 1 周、と分けて書いても動きます。ですが記録は同じなので、1 周の中で全部やってしまうほうが素直です。
def monthly_report(records, budget):
total = 0
by_category = {}
top = ""
top_price = -1
for record in records:
price = record["price"]
total = total + price
category = record["category"]
if category in by_category:
by_category[category] = by_category[category] + price
else:
by_category[category] = price
if price > top_price:
top_price = price
top = record["name"]
return {"total": total, "by_category": by_category, "top": top, "remaining": budget - total}by_category の 4 行は第 7 章そのままです。in でキーの有無を調べ、あれば足し、なければ新しく作ります。by_category[category] = by_category.get(category, 0) + price と 1 行で書いても構いません。
最高額の探し方
top_price を -1 から始めているのが大事なところです。金額は 0 円以上なので、1 件目は必ず price > top_price が成り立ち、top に品名が入ります。
ここを 0 から始めると、0 円の記録しかないときに 1 件も選ばれません。None から始める書き方もありますが、その場合は if top_price is None or price > top_price のように 2 段の判定が要ります。-1 にしておくと 1 行で済みます。
同じ金額が 2 件あったときは、条件が > なので 先に出てきたほう が残ります。>= にすると後のほうに入れ替わります。どちらでもよい仕様ですが、決めたら揃えます。
「同じ値だったらどうするか」を決めていないコードは、いつか必ず食い違います。仕様が曖昧なところこそ、自分で決めてコメントに残しておきます。
記録が 1 件もないとき
初回起動や、月が変わった直後は記録が空です。このときの戻り値を決めておかないと、画面が壊れます。
空リストのときは for の中身が 1 度も実行されないので、total は 0、by_category は {} のまま、top は "" のままになります。remaining は budget - 0 で予算そのものです。上のコードは、初期値を素直に置いているだけで空リストに正しく答えます。
monthly_report([], 3000)これは {"total": 0, "by_category": {}, "top": "", "remaining": 3000} を返します。
動きを追ってみる
記録が りんご 120円 食費、バス 220円 交通費、パン 180円 食費 の 3 件、予算が 3000 の場合を追います。
1 件目で total が 120、by_category が {"食費": 120}、top が "りんご" になります。2 件目で total が 340、by_category に "交通費" が加わり、220 > 120 なので top が "バス" に入れ替わります。3 件目で total が 520、食費 が 120 + 180 で 300 に増え、180 は 220 より小さいので top は "バス" のままです。
最後に remaining が 3000 - 520 で 2480 になり、{"total": 520, "by_category": {"食費": 300, "交通費": 220}, "top": "バス", "remaining": 2480} が返ります。
よくある間違い
remainingを合計の途中で計算する — ループの中で引くと、最後の記録を足す前の値が残ります。引き算はループを抜けたあとです。topに金額を入れてしまう —topは品名の文字列です。top_priceは探すための作業用の変数で、戻り値には入れません。- 予算オーバーを 0 で止める — 使いすぎたときの
remainingは負の数のままにします。-500と出るからこそ、いくら超えたのかが分かります。
やってみよう
関数 monthly_report(records, budget) を実装してください。records は支出の辞書のリスト、budget は予算の整数です。
{"total": 合計金額, "by_category": カテゴリ別の合計の辞書, "top": 最高額の品名, "remaining": 予算から合計を引いた値} の辞書を返します。同じ金額が並んだときは先に出てきたほうを top にします。記録が空のときは top を ""、by_category を {} にしてください。
要件
- 関数名は monthly_report で、引数は records と budget の2つ
- 戻り値は total と by_category と top と remaining の4つのキーを持つ辞書
- by_category は同じカテゴリの金額を足し合わせた辞書にする
- top は最高額の品名。同額なら先に出てきたほうを採用する
- 記録が空のときは top を空文字、by_category を空の辞書、remaining を budget にする
- 予算を超えたときも remaining は負の数のまま返す
入出力例
monthly_report([], 3000) → {"by_category":{},"remaining":3000,"top":"","total":0}
monthly_report([{"category":"食費","name":"りんご","price":120},{"category":"交通費","name":"バス","price":220},{"category":"食費","name":"パン","price":180}], 3000) → {"by_category":{"交通費":220,"食費":300},"remaining":2480,"top":"バス","total":520}
monthly_report([{"category":"娯楽","name":"映画","price":1800},{"category":"娯楽","name":"ゲーム","price":2500}], 3000) → {"by_category":{"娯楽":4300},"remaining":-1300,"top":"ゲーム","total":4300}
monthly_report([{"category":"日用品","name":"ノート","price":200},{"category":"日用品","name":"ペン","price":200}], 3000) → {"by_category":{"日用品":400},"remaining":2600,"top":"ノート","total":400}
monthly_report([{"category":"その他","name":"おまけ","price":0}], 3000) → {"by_category":{"その他":0},"remaining":3000,"top":"おまけ","total":0}
monthly_report([{"category":"食費","name":"牛乳","price":240}], 1000) → {"by_category":{"食費":240},"remaining":760,"top":"牛乳","total":240}