3.1 面向对象:用类重新组织账本


3.1 面向对象:用类重新组织账本

本节摘要:类把"数据"和"操作数据的函数"打包成一个整体,让数据终于有了唯一管理者。本节从 v0.2 的"ledger 裸奔"痛点出发,讲类的定义、构造方法、实例方法与封装,然后把轻记账重构出第一个类 Ledger——功能一行不减,代码归属清清楚楚。理解了本节,第 5 章的数据库模型与第 6 章的资源接口都只是它的变形。

动手前先定目标

阅读完本节,你应当能够:

  1. 定义类并区分类、实例、属性、方法四个词
  2. 写构造方法 init 与实例方法,解释 self 指向谁
  3. 说明封装解决什么问题,判断哪些数据该藏、哪些方法该露
  4. 把 v0.2 的散装函数重构为 Ledger 类,行为与原来完全一致

问题:数据在裸奔

v0.2 里,ledger 列表被 main、add_record、save_ledger 好几个函数传来传去。任何一处代码都能随手改它——传错参数、忘了落盘、改了结构没同步其他函数,全都没人拦。根子在于:数据和操作它的逻辑是分家的。分家导致"谁能改数据"没有边界。类给出的答案是把两者焊在一起:数据住在对象内部,只有它自己的方法能碰。外界想记账?调它的 add_record,规矩由它定。

图 3-1 从散装函数到 Ledger 类

图 3-1 从散装函数到 Ledger 类

类与实例:图纸和房子

类是图纸,实例是按图纸盖出来的房子。一本账是一个实例,程序里可以同时存在多个互不干扰的账本实例:

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 章兑现:账目从列表换成数据库表时,只要方法的输入输出不变,主程序一行不用改——封装是给未来留的活口

演练:v0.3 重构

把散装函数全部收编进类,主程序瘦身:

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 章多用户场景的雏形。

易错点清单

  • 忘写 self:实例方法参数里漏了 self,调用时报参数个数不符
  • 类属性与实例属性混淆:写在类体里的是全体实例共享的,写在 init 里才是每实例一份
  • 外界直接捅内部属性:绕过方法的校验和落盘,bug 埋得更深
  • all() 返回原列表:调用方改副本之外的"真身",封装形同虚设——返回副本

本节要点回顾

  • 类 = 数据 + 行为的打包,实例是图纸盖出的具体对象
  • init 负责初始化,self 指当前实例:定义带、调用不带
  • 封装原则:属性藏(下划线约定),行为露,规矩集中在方法里
  • Ledger 类完成 v0.3 重构,为换存储、上 Web 留好接口
  • 封装是给未来留的活口:内部随便换,门面不动

管家上岗了。可管家靠不靠谱,得有人验收——下一节调试与单元测试。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U