Skip to content

实战 · 命令行记账本 ​

🎯 难度:初级⏱ 预计:2 小时# 项目# JSON# Decimal

不接大模型,先把 Python 基础练扎实。成品支持新增、列表、汇总,退出重启后数据仍在。

你会做出什么? ​

text
$ uv run ledger.py add 19.90 午饭
已记录:19.90 元,午饭
$ uv run ledger.py list
1. 19.90 元 · 午饭
$ uv run ledger.py total
合计:19.90 元

拆解任务 ​

  1. 读命令行参数:相当于前端表单输入。
  2. 校验金额:非法数据不能入库。
  3. 从文件读取历史记录。
  4. 添加记录并写回文件。
  5. 从记录计算汇总。

钱用 Decimal 而不是 float,避免 0.1 + 0.2 这类误差。文件中保存金额字符串。

创建项目 ​

bash
uv init python-ledger --python 3.12
cd python-ledger

创建 ledger.py(只用标准库,不需要安装依赖):

python
import argparse
import json
from decimal import Decimal, InvalidOperation
from pathlib import Path

DB = Path(__file__).resolve().parent / "ledger.json"


def read_records() -> list[dict]:
    if not DB.exists():
        return []
    records = json.loads(DB.read_text(encoding="utf-8"))
    if not isinstance(records, list):
        raise ValueError("账本格式错误:应为列表")
    for row in records:
        if not isinstance(row, dict) or not {"amount", "note"} <= row.keys():
            raise ValueError("账本记录格式错误")
        value = Decimal(row["amount"])
        if not value.is_finite() or value <= 0:
            raise ValueError("账本含非法金额")
    return records


def parse_amount(raw: str) -> Decimal:
    amount = Decimal(raw)
    if not amount.is_finite() or amount <= 0 or amount > 1_000_000:
        raise ValueError("金额必须在 0 到 1,000,000 元之间")
    if amount != amount.quantize(Decimal("0.01")):
        raise ValueError("金额最多两位小数")
    return amount


def main():
    parser = argparse.ArgumentParser(description="命令行记账本")
    commands = parser.add_subparsers(dest="command", required=True)
    add = commands.add_parser("add", help="新增支出")
    add.add_argument("amount")
    add.add_argument("note")
    commands.add_parser("list", help="列出记录")
    commands.add_parser("total", help="汇总金额")
    args = parser.parse_args()

    try:
        records = read_records()
        if args.command == "add":
            amount = parse_amount(args.amount)
            records.append({"amount": f"{amount:.2f}", "note": args.note})
            # 先写临时文件,再替换,减少中途退出造成文件损坏的风险
            temp = DB.with_suffix(".tmp")
            temp.write_text(json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8")
            temp.replace(DB)
            print(f"已记录:{amount:.2f} 元,{args.note}")
        elif args.command == "list":
            for i, row in enumerate(records, start=1):
                print(f"{i}. {row['amount']} 元 · {row['note']}")
            if not records:
                print("暂无记录")
        else:
            total = sum((Decimal(row["amount"]) for row in records), Decimal("0"))
            print(f"合计:{total:.2f} 元")
    except (ValueError, InvalidOperation, TypeError, OSError) as error:
        parser.exit(1, f"操作失败:{error}\n")


if __name__ == "__main__":
    main()

逐段理解 ​

  • argparse:标准库的命令行解析器,--help 帮你自动生成帮助。
  • Path(__file__):数据固定在代码旁边,不受你从哪个目录运行的影响。
  • required=True:必须提供子命令,不能静默无动作。
  • try/except:JSON 损坏时拒绝继续写入,不吞掉错误。
  • temp.replace(DB):替换账本。这个小项目不支持多个进程同时写;多人应用需要数据库事务。

验收清单 ​

bash
uv run ledger.py add 19.90 午饭
uv run ledger.py add 8.50 咖啡
uv run ledger.py total       # 合计:28.40 元(全新账本)
uv run ledger.py add -1 测试  # 应拒绝
uv run ledger.py add abc 测试 # 应拒绝
uv run ledger.py add NaN 测试 # 应拒绝

关闭终端重新运行 list,数据仍应存在。将账本复制备份后试着破坏 JSON,程序应提示错误,而不是把原文件重置为空。

升级练习 ​

  1. 给记录加上 datetime 时间戳。
  2. 增加按类别汇总的命令。
  3. 给每条记录分配唯一 ID,再增加删除功能。

阶段通关

如果你能理解这份程序,并独立增加一个字段,就已经具备写第一个 Agent 的 Python 基础了。下一阶段:大模型核心概念。

用心写给每一个想入门 Agent 开发的前端工程师