第 9 课 / 共 9 课25 分钟2 个闯关

小项目:命令行记账本

字典 + 列表 + 函数 + 文件 + JSON 全用上,做一个真能用的东西。

把学过的东西拼成一个能用的东西

目标:一个命令行记账本。能记一笔支出、能看全部记录、能看总额,关掉程序数据还在。

用到的全部知识你都学过了:字典、列表、函数、循环、if、文件、JSON、异常。真实项目就是这么来的——没有新魔法,只是组合。

先想清楚数据长什么样

这是写代码前最该花时间的一步。一笔账需要:花了多少、花在哪、什么时候。

{"amount": 25.5, "category": "吃饭", "date": "2026-03-01"}

多笔账就是一个列表装一堆这样的字典:

[
  {"amount": 25.5, "category": "吃饭", "date": "2026-03-01"},
  {"amount": 8.0,  "category": "交通", "date": "2026-03-01"}
]

数据结构定对了,代码自然就顺了。 这句话你现在可能没感觉,写完这个项目会有。

完整代码

import json
import os
from datetime import date

DATA_FILE = "bills.json"


def load_bills():
    """从文件读出全部账目;文件不存在就返回空列表。"""
    if not os.path.exists(DATA_FILE):
        return []
    with open(DATA_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


def save_bills(bills):
    """把全部账目写回文件。"""
    with open(DATA_FILE, "w", encoding="utf-8") as f:
        json.dump(bills, f, ensure_ascii=False, indent=2)


def add_bill(bills):
    try:
        amount = float(input("金额:"))
    except ValueError:
        print("金额得是数字,这笔没记上")
        return
    category = input("分类(吃饭/交通/购物…):")
    bills.append({
        "amount": amount,
        "category": category,
        "date": str(date.today()),
    })
    save_bills(bills)
    print("记好了")


def show_bills(bills):
    if not bills:
        print("还没有任何记录")
        return
    for i, b in enumerate(bills, start=1):
        print(f'{i}. {b["date"]}  {b["category"]}  {b["amount"]:.2f} 元')


def show_summary(bills):
    total = sum(b["amount"] for b in bills)
    print(f"总支出:{total:.2f} 元")

    by_category = {}
    for b in bills:
        by_category[b["category"]] = by_category.get(b["category"], 0) + b["amount"]
    for name, amount in sorted(by_category.items(), key=lambda kv: -kv[1]):
        print(f"  {name}: {amount:.2f} 元")


def main():
    bills = load_bills()
    while True:
        print("\n1 记一笔  2 看记录  3 看统计  4 退出")
        choice = input("选择:").strip()
        if choice == "1":
            add_bill(bills)
        elif choice == "2":
            show_bills(bills)
        elif choice == "3":
            show_summary(bills)
        elif choice == "4":
            print("再见")
            break
        else:
            print("没这个选项")


main()

里面有三个值得你停下来看的写法

1. enumerate(bills, start=1) —— 同时拿下标和元素,并让下标从 1 开始。给人看的编号从 1 开始,程序内部从 0 开始,这是两回事。

2. sum(b["amount"] for b in bills) —— 这叫生成器表达式,等价于"把每个 b 的 amount 挑出来再求和"。展开写是四行循环,写成一行更清楚。

3. by_category.get(name, 0) + amount —— 分组统计的标准套路。第一次遇到某个分类时 get 返回默认值 0,于是不用提前判断"这个键在不在"。

自己动手改进它

按难度递增,挑着做:

  1. 加一个"删除某笔记录"的功能(提示:bills.pop(i) 然后 save_bills
  2. 统计只看本月(提示:比较 b["date"][:7]str(date.today())[:7]
  3. 金额不允许是负数或 0,不合法就提示重输
  4. main() 换成 if __name__ == "__main__": main() —— 去问 AI 私教这行是什么意思,它是 Python 项目的标准写法

10 秒自测

如果删掉 save_bills(bills) 那一行,程序还能正常记账吗?

答案是:当次能,重开就全没了。内存里的列表更新了,但没写回文件。这就是"持久化"的意义。

广告位(未配置 AdSense,上线后在此展示)

动手闯关

读懂了不算会,写出来才算

0/2 已通过

闯关和判题完全免费,交错了不扣任何东西。卡住随时点「问 AI」,它能看到你正在写的代码。

关卡 01写代码+25 点 · +40 XP

算出总支出和最大的一笔

给定 bills 列表,输出两行: 第一行 `总计:金额`(保留两位小数) 第二行 `最大:分类`(金额最大那笔的分类)

示例输入

(无)

应该输出

总计:232.50
最大:购物
pythonTab 缩进 4 空格
关卡 02选择+10 点 · +15 XP

为什么数据会丢

记账程序里如果删掉 `save_bills(bills)` 这一行,会发生什么?

  • 可能是多选,选错不扣点数,随便试。
登录后闯关拿点数

学完了?标记一下

标记完成会记进度、涨 50 XP,还会点亮连续学习天数。