本节摘要:类把"数据"和"操作数据的函数"打包成一个整体,让数据终于有了唯一管理者。本节从 v0.2 的"ledger 裸奔"痛点出发,讲类的定义、构造方法、实例方法与封装,然后把轻记账重构出第一个类 Ledger——功能一行不减,代码归属清清楚楚。理解了本节,第 5 章的数据库模型与第 6 章的资源接口都只是它的变形。
阅读完本节,你应当能够:
v0.2 里,ledger 列表被 main、add_record、save_ledger 好几个函数传来传去。任何一处代码都能随手改它——传错参数、忘了落盘、改了结构没同步其他函数,全都没人拦。根子在于:数据和操作它的逻辑是分家的。分家导致"谁能改数据"没有边界。类给出的答案是把两者焊在一起:数据住在对象内部,只有它自己的方法能碰。外界想记账?调它的 add_record,规矩由它定。

类是图纸,实例是按图纸盖出来的房子。一本账是一个实例,程序里可以同时存在多个互不干扰的账本实例:
class Ledger: """一个记账本:内部管理账目数据,对外提供操作方法。""" def __init__(self): self.records = [] # 实例属性:每个账本对象各有一份 def add_record(self, item, amount): self.records.append({"item": item, "amount": amount}) def month_total(self, month): return sum(r["amount"] for r in self.records)
__init__ 是构造方法,创建实例时自动执行,负责给对象"开机初始化";self 指向当前这个实例——my_ledger.add_record(...) 执行时,self 就是 my_ledger,所以方法里读写 self.records 改的正是这一个账本的列表。定义时的 self 看着多余,调用时它由 Python 自动传入,这是新手最容易懵的一点:定义带 self,调用不带。
my_ledger = Ledger() # 创建实例,__init__ 自动执行 my_ledger.add_record("早饭", 12.5) work_ledger = Ledger() # 第二个实例,records 独立 print(len(my_ledger.records)) # 1 print(len(work_ledger.records)) # 0 —— 互不干扰
封装的原则一句话:属性藏起来,行为露出去。外界不该直接捅 my_ledger.records.append(...)——绕过了 add_record 的规矩(比如将来加的日期、校验)。Python 没有强制私有,靠约定:属性名前加单个下划线表示"内部用,外人别碰":
class Ledger: def __init__(self): self._records = [] # 下划线:内部属性 def add_record(self, item, amount): if amount <= 0: raise ValueError("金额必须为正") # 规矩集中在一个地方 self._records.append({ "item": item, "amount": amount, "date": date.today().isoformat(), }) @property def count(self): # 只读属性,像字段一样用 return len(self._records) ledger = Ledger() ledger.add_record("早饭", 12.5) print(ledger.count) # 1 —— 外界不知道、也不需要知道内部是列表还是别的
这层"门面"的价值在第 5 章兑现:账目从列表换成数据库表时,只要方法的输入输出不变,主程序一行不用改——封装是给未来留的活口。
把散装函数全部收编进类,主程序瘦身:
from datetime import date import json class Ledger: def __init__(self, data_file="ledger.json"): self._data_file = data_file self._records = self._load() def _load(self): try: with open(self._data_file, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return [] def save(self): with open(self._data_file, "w", encoding="utf-8") as f: json.dump(self._records, f, ensure_ascii=False, indent=2) def add_record(self, item, amount): if amount <= 0: raise ValueError("金额必须为正") self._records.append({ "item": item, "amount": amount, "date": date.today().isoformat(), }) self.save() def all(self): return list(self._records) # 返回副本,防止外界乱改 ledger = Ledger() try: ledger.add_record("早饭", 12.5) except ValueError as e: print(e) for r in ledger.all(): print(r["item"], r["amount"])
注意 __init__ 里调 self._load——方法之间也能通过 self 互相调用,类的内部是一个可以自由协作的小团队。变式一:给 Ledger 加 delete_by_index(i) 方法,删除前检查序号范围。变式二:创建两个 Ledger 实例指向不同数据文件,验证互不干扰——这正是第 6 章多用户场景的雏形。
管家上岗了。可管家靠不靠谱,得有人验收——下一节调试与单元测试。