自由拡張
このレッスンで作るもの — 品名の一部から支出を探し出す
search_recordsを作ります。そのあと、自分ならこのアプリに何を足すかを考える回です。
自由拡張
記録して、集計して、レポートまで出せるようになりました。ここまで来ると、使っているうちに「これができたらいいのに」が出てきます。今回はその第一歩として検索を足し、そのあと拡張の考え方を扱います。
検索が欲しくなる理由
記録が 100 件になると、一覧を目で追うのがつらくなります。「先月コーヒーにいくら使ったか」を知りたいだけなのに、全部を眺める羽目になります。
欲しいのは「品名に コーヒー を含む記録だけ」を取り出す機能です。完全一致ではなく 部分一致 にするのが肝で、アイスコーヒー も コーヒー豆 も一緒に拾えます。
検索は、絞り込みの道具であると同時に、記録が増えても壊れないことの証明でもあります。件数が増えるほど価値が出る機能です。
in で部分一致を調べる
第 3 章で in を「リストに含まれるか」の判定として使いました。同じ in は、文字列に対しても使えます。
"コーヒー" in "アイスコーヒー"これは True になります。左が右のどこかに現れれば真、という意味です。左右の順番を逆にすると別の意味になるので、キーワード in 品名 の向きを守ります。
あとは第 5 章の for と第 7 章の append を組み合わせるだけです。
def search_records(records, keyword):
found = []
for record in records:
if keyword in record["name"]:
found.append(record)
return found第 7 章のリスト内包表記で書くと 1 行になります。どちらでも構いません。
def search_records(records, keyword):
return [record for record in records if keyword in record["name"]]見つからなかったとき
該当がゼロなら、空のリスト [] を返します。None を返してはいけません。呼び出す側が len() や for をそのまま使えるほうが扱いやすいからです。
search_records([], "コーヒー")記録が空なら当然 [] です。空のリストは if found: が偽になるので、呼び出し側は「見つかりませんでした」の表示に素直に分岐できます。
もうひとつ、キーワードが空文字 "" のときは全件が返ります。"" in "りんご" が True になるためです。これは Python の仕様で、直したいなら呼び出す前に空かどうかを確かめます。仕様として受け入れるなら、そのままで構いません。
空の結果を
Noneで返す設計は、呼び出し側にif 結果 is Noneの分岐を強制します。同じ「無い」を表すなら、型を変えない[]のほうが後々楽です。
動きを追ってみる
記録が アイスコーヒー 480円 娯楽、りんご 120円 食費、コーヒー豆 900円 食費 の 3 件で、キーワードが コーヒー の場合を追います。
1 件目の品名は アイスコーヒー で、コーヒー を含むので found に入ります。2 件目の りんご は含まないので飛ばします。3 件目の コーヒー豆 は含むので入ります。結果は 1 件目と 3 件目の 2 件です。
このとき返っているのは記録そのもの、つまり辞書です。金額もカテゴリも付いたままなので、monthly_report にそのまま渡せます。「コーヒーだけの月間レポート」が、新しいコードなしで出せることになります。
次に何を足すか
検索ができたら、次は自分で決める番です。考えられる拡張を挙げておきます。
- 日付を持たせる — 記録に
"date"を足して{"name": "りんご", "price": 120, "category": "食費", "date": "2026-08-05"}にします。並べ替えも期間の絞り込みもできるようになります。 - 月をまたいで集計する — 日付があれば、月ごとにまとめられます。
monthly_reportを月の指定つきに拡張し、先月との差を出すところまで進めます。 - グラフにする — カテゴリ別の合計を
*の数で表すだけでも棒グラフになります。"食費" + " " + "*" * (300 // 100)のような組み立てで、追加のライブラリなしに作れます。 - CSV にして Excel で開く — 第 8 章の
save_and_loadを品名,金額,カテゴリの行に変えれば、そのまま表計算ソフトで開けます。手元の家計簿として本当に使えるようになります。
どれも、これまでに習った文法だけで作れます。新しい知識を足す前に、持っている道具で作れる範囲を広げるほうが力が付きます。
よくある間違い
inの向きを逆にする —record["name"] in keywordと書くと、品名がキーワードに含まれるかという逆の判定になります。ほとんどの場合ずっと偽になります。- 見つからないときに
Noneを返す — 呼び出し側がforで回した瞬間にTypeErrorになります。空のリストを返します。 - 元のリストから消しながら探す —
removeで絞り込もうとすると、元の記録が壊れます。新しいリストに集めるのが安全です。
やってみよう
関数 search_records(records, keyword) を実装してください。records は支出の辞書のリスト、keyword は探したい文字列です。
品名に keyword を含む記録だけを、元の順番のまま新しいリストにして返します。1 件も見つからないときと、records が空のときは、どちらも空のリストを返してください。返すのは辞書そのもので、品名だけを取り出したりはしません。
要件
- 関数名は search_records で、引数は records と keyword の2つ
- 品名に keyword を含む記録だけを集める。完全一致ではなく部分一致にする
- 返すのは記録の辞書そのもの。品名だけを取り出さない
- 元の並び順を変えない
- 1件も見つからないときと records が空のときは、空のリストを返す
入出力例
search_records([{"category":"娯楽","name":"アイスコーヒー","price":480},{"category":"食費","name":"りんご","price":120},{"category":"食費","name":"コーヒー豆","price":900}], "コーヒー") → [{"category":"娯楽","name":"アイスコーヒー","price":480},{"category":"食費","name":"コーヒー豆","price":900}]
search_records([{"category":"食費","name":"りんご","price":120},{"category":"交通費","name":"バス","price":220}], "うどん") → []
search_records([], "コーヒー") → []
search_records([{"category":"食費","name":"りんご","price":120},{"category":"交通費","name":"バス","price":220}], "") → [{"category":"食費","name":"りんご","price":120},{"category":"交通費","name":"バス","price":220}]
search_records([{"category":"日用品","name":"ノート","price":200},{"category":"日用品","name":"ペン","price":150}], "ノート") → [{"category":"日用品","name":"ノート","price":200}]
search_records([{"category":"食費","name":"パン","price":180},{"category":"食費","name":"食パン","price":250},{"category":"食費","name":"牛乳","price":240},{"category":"食費","name":"菓子パン","price":130}], "パン") → [{"category":"食費","name":"パン","price":180},{"category":"食費","name":"食パン","price":250},{"category":"食費","name":"菓子パン","price":130}]