| Day 21 | 阶段三项目:命令行记账本 |

1 项目总览 —— 命令行记账本(阶段三收官)

是什么?为什么学?

今天是阶段三收官日,没有新语法——把所有知识串起来做一个命令行记账本。前 20 天学了语法、函数、文件、面向对象、标准库,今天把它组装成一个「真正能用的」命令行记账本:要有清晰的类设计、能交互操作、数据关机不丢。做完这个项目,你就具备了「独立把一个小需求做成程序」的能力。

阶段知识地图

Day 15  类与对象(class/属性/方法/__init__) ────────┐
Day 16  继承与多态(子类复用/重写/鸭子类型) ─────────┐│
Day 17  魔法方法(__str__/__len__/__eq__/运算符重载) ─┐││
Day 18  迭代器、生成器与装饰器(惰性求值/@语法糖) ────┐│││
Day 19  常用标准库(datetime/collections/itertools/re)┐││││
Day 20  第三方库(pip/venv/requests/beautifulsoup4) ──┐│││││
                                                    └─┴┴┴┴┴┴──→ 阶段项目:命令行记账本

一句话串法:类把数据和函数打包 → 继承让类复用 → 魔法方法让对象「像内置类型」→ 生成器装饰器让代码更优雅 → 标准库提供现成工具 → 第三方库扩展无限能力。记账本就是把这些零件按顺序组装起来:类当骨架、魔法方法当「打印门面」、datetime 记时间、json 做保险柜、异常处理当安全气囊。

注意事项总结

1 项目拆解表(四大功能 → 知识点)

功能用到的知识点具体写法来自哪一天
添加收支类与对象、魔法方法、datetime、异常处理Record(rtype, category, amount, note)datetime.now() 打时间戳、try/except ValueError 拦非法金额Day 15/17/19/13
查看流水类与对象、魔法方法for r in book.records: print(r) 触发 Record.__str__Day 15/17
统计余额类与对象、生成器表达式sum(r.amount for r in self.records if r.rtype == "支出")Day 15/18
分类统计类与对象、列表推导式[r for r in self.records if r.category == category]Day 15/6

2 持久化与交互 → 知识点

功能用到的知识点具体写法来自哪一天
保存数据标准库 json、文件读写json.dump(data, f, ensure_ascii=False, indent=2)Day 19/12/11
加载数据文件判断、json、异常处理os.path.exists + json.load + except (json.JSONDecodeError, OSError)Day 10/12/13
序列化转换魔法方法、类方法Record.to_dict()(转字典)、Record.from_dict()(@classmethod 转回对象)Day 17/15
主菜单while 循环、条件判断while True: + if/elif + breakDay 4/5
导出 CSV标准库 csv、编码csv.DictWriter + encoding="utf-8-sig"Day 19/12

易错点

  • 错误写法:一口气写完 150 行代码再运行(「憋大招」) → 问题:一运行全是错,不知道从哪改起 → 正确写法:三步走(骨架 → 填空腹 → 加持久化防护),每步都能运行验证。
  • 错误写法:项目做不出来就怀疑是今天的问题 → 问题:往往是前面某天基础没打牢 → 正确写法:对照拆解表自查「这一步用到了哪个知识点」,回去翻那一天的总结。

记忆口诀:类当骨架,魔法方法当门面;datetime 记时间,json 做保险柜;拆解表对号入座,小步迭代不憋大招。

2 需求分析与类设计 —— Record 与 Book

是什么?为什么学?

先想清楚要什么功能:添加收入/支出、查看全部流水、统计余额、按类别筛选、保存数据。程序里有两个天然的概念,拆成两个类:Record一条收支记录)和 Book整个账本)。一个类管「个体」,一个类管「整体」,职责清晰。

注意事项总结

1 Record 类——一条收支记录

class Record:
    """一条收支记录"""

    def __init__(self, rtype, category, amount, note="", date=None):
        if rtype not in ("收入", "支出"):
            raise ValueError("类型只能是 收入 或 支出")
        self.rtype = rtype            # 收入 / 支出
        self.category = category      # 餐饮、交通、工资……
        self.amount = float(amount)   # 金额
        self.note = note              # 备注
        self.date = date or "2026-08-03 09:00:00"

    def __str__(self):
        symbol = "+" if self.rtype == "收入" else "-"
        return f"[{self.date}] {self.rtype} {self.category} {symbol}{self.amount:.2f} {self.note}"


r = Record("支出", "餐饮", 25.5, "午饭")
print(r)

运行结果:

[2026-08-03 09:00:00] 支出 餐饮 -25.50 午饭

__init__ 里先校验类型(非法直接 raise ValueError);__str__ 决定打印格式,f"{self.amount:.2f}" 金额两位小数,symbol 用条件表达式决定 + 还是 -

2 Book 类——添加、统计、筛选

class Book:
    def __init__(self):
        self.records = []

    def add_record(self, record):
        self.records.append(record)

    def balance(self):
        """收入加起来减支出,得到余额"""
        total = 0.0
        for r in self.records:
            if r.rtype == "收入":
                total += r.amount
            else:
                total -= r.amount
        return total

    def filter_by_category(self, category):
        return [r for r in self.records if r.category == category]

balance() 遍历所有记录,收入加、支出减;filter_by_category 用列表推导式筛出同类记录。

易错点

  • 错误写法:一个类装所有东西(一条记录和整个账本混在一个类里) → 问题:职责混乱,改一个地方影响全局 → 正确写法:拆成 Record(个体)和 Book(整体),各管各的。

记忆口诀:个体 Record,整体 Book,一个管一条、一个管全本;字段校验放构造,打印交给 __str__。

3 魔法方法与生成器 —— 对象的「门面」和「统计」

是什么?为什么学?

__str__ 决定「打印一条记录」长什么样——查看流水时 print(r) 直接输出可读的一行;生成器表达式让「筛选 + 求和」一行搞定,不产生中间列表。

注意事项总结

1 str 让「打印一条记录」一行搞定

class Record:
    def __init__(self, rtype, category, amount, note="", date="2026-08-04 09:30:00"):
        self.rtype = rtype
        self.category = category
        self.amount = float(amount)
        self.note = note
        self.date = date

    def __str__(self):
        symbol = "+" if self.rtype == "收入" else "-"
        return f"[{self.date}] {self.rtype} {self.category} {symbol}{self.amount:.2f} {self.note}"

r = Record("支出", "餐饮", 25.5, "午饭")
print(r)   # 触发 __str__

运行结果:

[2026-08-04 09:30:00] 支出 餐饮 -25.50 午饭

没有 __str__,打印对象只会看到 <__main__.Record object at 0x...>,完全没法看

2 生成器表达式统计支出总额(惰性、一行)

class Book:
    def __init__(self):
        self.records = []

    def expense_total(self):
        return sum(r.amount for r in self.records if r.rtype == "支出")

book = Book()
book.records = [Record("支出", "餐饮", 25.5), Record("收入", "工资", 8000)]
print("支出总额:", book.expense_total())

运行结果:

支出总额: 25.5

(r.amount for r in self.records if r.rtype == "支出") 是生成器表达式——边遍历边过滤,不产生中间列表sum() 消费它算出总和。

3 datetime 自动打时间戳

from datetime import datetime

class Record:
    def __init__(self, rtype, category, amount, note=""):
        self.rtype = rtype
        self.category = category
        self.amount = float(amount)
        self.note = note
        self.date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

datetime.now() 获取当前时刻,strftime("%Y-%m-%d %H:%M:%S") 格式化成字符串存进 self.date——每条记录自动盖上时间戳,不用手动输入时间

易错点

  • 错误写法:忘了实现 __str__ 直接 print(r) → 问题:输出内存地址,完全没法看 → 正确写法:任何自定义类都实现 __str__
  • 错误写法:手写 for + if + 累加做统计 → 问题:代码长且容易错 → 正确写法:生成器表达式 + sum() 一行搞定。

记忆口诀str 定门面,打印变人话;生成器一行统计,datetime 自动打戳。

4 持久化与序列化 —— 对象 ↔ 字典 ↔ JSON

是什么?为什么学?

程序关了数据就没了,这不行。用 json 模块把记录列表转成文本存进文件,启动时再读回来。但 JSON 只认 dict/list/str/num,不认对象——所以需要 Record.to_dict()(存之前转字典)和 Record.from_dict()(读出来转回对象),打通「对象 ↔ 字典 ↔ JSON」。

注意事项总结

1 保存和加载的基本姿势

import json

records = [
    {"rtype": "收入", "category": "工资", "amount": 8000, "note": "月薪"},
    {"rtype": "支出", "category": "餐饮", "amount": 25.5, "note": "午饭"},
]

# 保存:ensure_ascii=False 让中文正常显示,indent=2 格式化
with open("records.json", "w", encoding="utf-8") as f:
    json.dump(records, f, ensure_ascii=False, indent=2)

# 加载
with open("records.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

print("记录数:", len(loaded))

2 to_dict / from_dict 配合(@classmethod 工厂方法)

import json


class Record:
    def __init__(self, rtype, category, amount, note=""):
        self.rtype = rtype
        self.category = category
        self.amount = float(amount)
        self.note = note

    def to_dict(self):
        return {"rtype": self.rtype, "category": self.category,
                "amount": self.amount, "note": self.note}

    @classmethod
    def from_dict(cls, data):
        return cls(data["rtype"], data["category"], data["amount"], data.get("note", ""))


records = [Record("收入", "工资", 8000)]
data = [r.to_dict() for r in records]
with open("records.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

with open("records.json", "r", encoding="utf-8") as f:
    back = [Record.from_dict(item) for item in json.load(f)]

print(back[0].rtype, back[0].category, back[0].amount)

运行结果:

收入 工资 8000.0

to_dict() 把对象转成字典(存文件前调用);from_dict() 把字典转回对象(加载时调用)。@classmethodfrom_dict不创建实例就通过类调用(Record.from_dict(data)),返回的是 cls(...) 构造的新对象——这就是 Day 18 的「工厂方法」。

易错点

  • 错误写法:直接把 Record 对象丢给 json.dump → 问题:报 TypeError: Object of type Record is not JSON serializable → 正确写法:先 to_dict() 转成字典再存。
  • 错误写法:读写文件没指定编码 → 问题:中文乱码或报 UnicodeDecodeError → 正确写法:读写都加 encoding="utf-8"

记忆口诀:JSON 只认字典,对象先 to_dict;读回用 from_dict,工厂方法 @classmethod。

5 动手实践 —— 完整命令行记账本

把四个部分组合成完整程序,直接保存为 account_book.py 运行即可。功能包含:添加收支、查看全部流水、统计余额、按类别筛选、保存到 JSON、导出 CSV、退出时自动保存。

# account_book.py —— 命令行记账本
import csv
import json
import os
from datetime import datetime

DATA_FILE = "records.json"
CATEGORIES = ["餐饮", "交通", "购物", "娱乐", "工资", "其他"]


class Record:
    """一条收支记录"""

    def __init__(self, rtype, category, amount, note="", date=None):
        if rtype not in ("收入", "支出"):
            raise ValueError("类型只能是 收入 或 支出")
        self.rtype = rtype
        self.category = category
        self.amount = float(amount)
        self.note = note
        self.date = date or datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    def to_dict(self):
        return {
            "rtype": self.rtype,
            "category": self.category,
            "amount": self.amount,
            "note": self.note,
            "date": self.date,
        }

    @classmethod
    def from_dict(cls, data):
        return cls(
            data["rtype"],
            data["category"],
            data["amount"],
            data.get("note", ""),
            data.get("date", ""),
        )

    def __str__(self):
        symbol = "+" if self.rtype == "收入" else "-"
        return f"[{self.date}] {self.rtype} {self.category} {symbol}{self.amount:.2f} {self.note}"


class Book:
    """账本:管理所有收支记录"""

    def __init__(self):
        self.records = []

    def add_record(self, record):
        self.records.append(record)

    def balance(self):
        total = 0.0
        for r in self.records:
            if r.rtype == "收入":
                total += r.amount
            else:
                total -= r.amount
        return total

    def filter_by_category(self, category):
        return [r for r in self.records if r.category == category]

    def save_json(self, path=DATA_FILE):
        data = [r.to_dict() for r in self.records]
        with open(path, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"已保存 {len(self.records)} 条记录到 {path}")

    def save_csv(self, path="records.csv"):
        with open(path, "w", encoding="utf-8-sig", newline="") as f:
            writer = csv.DictWriter(
                f, fieldnames=["date", "rtype", "category", "amount", "note"]
            )
            writer.writeheader()
            for r in self.records:
                writer.writerow({
                    "date": r.date,
                    "rtype": r.rtype,
                    "category": r.category,
                    "amount": r.amount,
                    "note": r.note,
                })
        print(f"已导出 {len(self.records)} 条记录到 {path}")

    def load_json(self, path=DATA_FILE):
        if not os.path.exists(path):
            print("没有找到存档文件,从空账本开始")
            return
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        self.records = [Record.from_dict(item) for item in data]
        print(f"已加载 {len(self.records)} 条历史记录")


def main():
    book = Book()
    book.load_json()

    while True:
        print("\n===== 命令行记账本 =====")
        print("1. 添加收入")
        print("2. 添加支出")
        print("3. 查看全部流水")
        print("4. 查看余额")
        print("5. 按类别筛选")
        print("6. 保存到 JSON")
        print("7. 导出 CSV")
        print("0. 退出(自动保存)")
        choice = input("请选择操作: ").strip()

        if choice in ("1", "2"):
            rtype = "收入" if choice == "1" else "支出"
            category = input(f"类别({','.join(CATEGORIES)}): ").strip()
            try:
                amount = float(input("金额: ").strip())
            except ValueError:
                print("金额必须是数字,请重新操作")
                continue
            note = input("备注(可留空): ").strip()
            book.add_record(Record(rtype, category, amount, note))
            print("添加成功")

        elif choice == "3":
            if not book.records:
                print("暂无记录")
            else:
                for r in book.records:
                    print(r)

        elif choice == "4":
            print(f"当前余额: {book.balance():.2f}")

        elif choice == "5":
            category = input(f"输入类别({','.join(CATEGORIES)}): ").strip()
            found = book.filter_by_category(category)
            if not found:
                print("该类别暂无记录")
            else:
                for r in found:
                    print(r)

        elif choice == "6":
            book.save_json()

        elif choice == "7":
            book.save_csv()

        elif choice == "0":
            book.save_json()
            print("再见,欢迎下次使用")
            break

        else:
            print("无效选择,请重试")


if __name__ == "__main__":
    main()

运行一次完整流程(第一次运行):

没有找到存档文件,从空账本开始

===== 命令行记账本 =====
1. 添加收入
2. 添加支出
3. 查看全部流水
4. 查看余额
5. 按类别筛选
6. 保存到 JSON
7. 导出 CSV
0. 退出(自动保存)
请选择操作: 1
类别(餐饮,交通,购物,娱乐,工资,其他): 工资
金额: 8000
备注(可留空): 8月工资
添加成功

请选择操作: 3
[2026-08-03 15:30:00] 收入 工资 +8000.00 8月工资
[2026-08-03 15:32:00] 支出 餐饮 -25.50 午饭

请选择操作: 4
当前余额: 7974.50

请选择操作: 0
已保存 2 条记录到 records.json
再见,欢迎下次使用

再次运行程序会自动加载 records.json,历史记录都在。CSV 导出后可以用 Excel 打开(utf-8-sig 编码保证 Excel 中文不乱码)。

升级挑战:加「删除某条记录」功能(用序号删);加「按月统计」功能(把 Record.date 前 7 位 2026-08 作为月份分组统计)。

6 开发流程 —— 三步搭起记账本

是什么?为什么学?

不要一口气写完 150 行代码——新手最常见的失败方式就是「憋大招」:写了几十行没运行过一次,一运行全是错,根本不知道从哪改起。正确做法是三步走,每步都能运行、都能验证

注意事项总结

1 第 1 步:搭骨架——先让菜单跑起来

class Record:
    pass

class Book:
    pass

def main():
    while True:
        choice = input("请选择操作(0 退出): ")
        if choice == "0":
            print("再见")
            break
        elif choice == "1":
            print("(功能开发中)")
        else:
            print("无效选择,请重试")

if __name__ == "__main__":
    main()

pass 是空语句,让类能通过语法检查。这一步的意义:先把「程序的形状」立起来,错误范围最小

2 第 2 步:填空腹——先做「添加」和「查看」:把 RecordBookpass 换成真实逻辑,加上添加、查看、余额三个功能,跑一遍:能记一笔、能看流水、余额算得对。

3 第 3 步:加持久化和防护——保存/加载 + 异常处理:最后补上 save_json / load_json / save_csv,再给金额输入加上 try / except ValueError。这步是「锦上添花 + 加固」。

易错点

  • 错误写法:一次写完所有功能再运行 → 问题:出错范围大,难以定位 → 正确写法:每完成一步就跑一次、验证一次——出问题范围小、好定位。
  • 错误写法:跳过骨架直接写功能 → 问题:菜单逻辑和功能逻辑混在一起 → 正确写法:先让菜单循环跑通(用 pass 占位),再逐个填空。

记忆口诀:先骨架后填空,再加固;每步都运行,小步快迭代。

今日总结

阶段知识地图:类 → 继承多态 → 魔法方法 → 生成器装饰器 → 标准库 → 第三方库,一环扣一环 需求分析先行:区分「一条记录」(Record)和「整个账本」(Book),职责清晰 __str__ 定打印门面:print(r) 直接输出可读流水;没有它只能看到内存地址 生成器表达式一行统计:sum(r.amount for r in self.records if r.rtype == "支出") datetime.now().strftime(...) 自动打时间戳,不用手动输入时间 持久化闭环:Record.to_dict()(转字典)→ json.dump 保存 → json.load 加载 → Record.from_dict()(@classmethod 转回对象) 金额输入用 try/except ValueError 拦非法输入,程序不闪退 导出 CSV 用 utf-8-sig 编码,Excel 打开中文不乱码 开发流程:三步走(骨架 → 填空腹 → 加持久化防护),每步都能运行验证 独立完成命令行记账本,测试非法输入、重启加载、CSV 导出等场景

报错原因修复
ValueError: could not convert string to float: 'abc'金额输入了非数字try/except 包住 float(input(...)),输错提示重来
json.decoder.JSONDecodeError存档文件被改坏或为空删除 records.json 重新开始;保存时确认用了 json.dump
中文乱码读写文件没指定编码读写都加 encoding="utf-8";导 CSV 用 utf-8-sig
TypeError: 'float' object is not subscriptable把金额当列表用了(如 amount[0]金额是数字,检查是否误用下标
数据没保存忘了调用 save_json,或退出分支没触发保存0 退出分支里调用 book.save_json()
TypeError: Object of type Record is not JSON serializable直接把 Record 对象丢给 json.dumpto_dict() 转成字典再存
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇