主题
实战 · 命令行记账本
不接大模型,先把 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 元拆解任务
- 读命令行参数:相当于前端表单输入。
- 校验金额:非法数据不能入库。
- 从文件读取历史记录。
- 添加记录并写回文件。
- 从记录计算汇总。
钱用 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,程序应提示错误,而不是把原文件重置为空。
升级练习
- 给记录加上
datetime时间戳。 - 增加按类别汇总的命令。
- 给每条记录分配唯一 ID,再增加删除功能。
阶段通关
如果你能理解这份程序,并独立增加一个字段,就已经具备写第一个 Agent 的 Python 基础了。下一阶段:大模型核心概念。