面向对象高级(实战案例)

我用一章学的东西写了个图书借阅系统|Python 面向对象高级(5):综合实战

系列说明:第十一章「面向对象高级」学习笔记最后一篇。前四篇分别是封装、继承与方法重写、多继承与 MRO、多态与鸭子类型。这一篇把它们拼成一个能跑的程序。

案例名称:《书海灯塔 · 命令行图书借阅系统》

需求清单很直接:会员凭卡号密码登录,登录后可以借书、还书、查看自己的借阅,数据存 JSON。但里面藏着一条专门为多态准备的规则:

  • 普通会员最多借 3 本。

  • VIP 会员最多借 6 + VIP等级 本(未指定等级默认为 1)。

  • 图书可用数量 > 0 才能借;借出减 1,归还加 1。

  • 已经借满的人不能再借。

一看到"不同会员上限不同",我就明白了:这就是上一篇的 member.get_max_borrow(),绝对不该写成 if type(member) == VIPMember。

类结构设计

我设计了五个类,职责分成两层:

  • 业务对象层(是什么) :Book 封装编号、书名、作者、总数、可用数和借还动作;Member 会员父类封装私有的卡号、密码、姓名和公共借阅记录;NormalMember 和 VIPMember 继承父类,各自重写借阅上限。

  • 系统层(怎么运作) :LibrarySystem 管 JSON 读写、登录、菜单循环、借书、还书、查看借阅。它只跟 Member 这个父类打交道,完全不知道有几种会员。

会员这一支,正好把整章串了一遍:卡号密码用 __ 私有化(封装),两个子类继承父类(继承),上限方法各自重写、系统只调父类签名(多态)。

数据文件设计

首次运行会生成两个 JSON,之后所有借还都写回它们。

books.json:

{"book_id": "B001", "title": "三体", "author": "刘慈欣", "total": 5, "available": 5}, {"book_id": "B002", "title": "平凡的世界", "author": "路遥", "total": 3, "available": 3}, {"book_id": "B003", "title": "战争与和平", "author": "托尔斯泰", "total": 2, "available": 2}, {"book_id": "B004", "title": "我是猫", "author": "夏目漱石", "total": 1, "available": 1}, {"book_id": "B005", "title": "唐诗三百首(绝版典藏)", "author": "佚名", "total": 2, "available": 0}

members.json(跑完一轮后的样子):

{ "type": "normal", "card_no": "1001", "password": "123", "name": "张三", "borrowed_ids": \["B001", "B003", "B004"

},

{

"type": "vip",

"card_no": "9001",

"password": "vip888",

"name": "王五",

"borrowed_ids": "B001",

"vip_level": 2

}

]

type 字段是我特意加的:读回来时靠它决定实例化哪个子类。而 borrowed_ids 只存图书编号,不存整本书的信息------避免数据冗余,改库存时不会出现"张三手里那本《三体》还剩几本"和图书馆实际库存不一致。

核心代码:会员与图书

by tryer07 | Python 面向对象高级(5)综合实战 · 学习笔记

class Book:

参数与属性显式标注类型,避免后续 min() 推导出未知的协议联合类型导致连锁报错

def init(self, book_id: str, title: str, author: str, total: int, available: int):

self.book_id = book_id

self.title = title

self.author = author

self.total: int = total

self.available: int = min(available, total) # 可用数不得大于总数

def is_available(self) -> bool:

return self.available > 0

def borrow(self) -> None:

self.available -= 1

def return_back(self) -> None:

self.available = min(self.available + 1, self.total)

def to_dict(self) -> dict:

return {"book_id": self.book_id, "title": self.title, "author": self.author,

"total": self.total, "available": self.available}

class Member:

卡号、密码、姓名全部私有

def init(self, card_no: str, password: str, name: str):

self.__card_no: str = card_no

self.__password: str = password

self.__name: str = name

self.borrowed_books: listBook = \[\]

def get_card_no(self) -> str:

return self.__card_no

def get_name(self) -> str:

return self.__name

def check_password(self, password: str) -> bool:

return password == self.__password

父类不定义具体规则,强制子类重写

def get_max_borrow(self) -> int:

return 0

def is_borrow_full(self) -> bool:

return len(self.borrowed_books) >= self.get_max_borrow()

def to_dict(self) -> dict:

return {

"type": "normal",

"card_no": self.__card_no,

"password": self.__password,

"name": self.__name,

"borrowed_ids": book.book_id for book in self.borrowed_books,

}

class NormalMember(Member):

def get_max_borrow(self) -> int:

return 3

class VIPMember(Member):

def init(self, card_no: str, password: str, name: str, vip_level: int = 1):

super().init(card_no, password, name)

self.__vip_level: int = vip_level

def get_max_borrow(self) -> int:

return 6 + self.__vip_level

def to_dict(self) -> dict:

data = super().to_dict()

data"type" = "vip"

data"vip_level" = self.__vip_level

return data

几个我觉得很妙的设计点:
  1. get_max_borrow() 父类故意返回 0 :父类不知道该定多少,但必须有个方法占位,否则 is_borrow_full() 里调它就会报错。返回 0 意味着"父类对象一本都借不了",把"必须由子类给出真实规则"变成了硬约束。

  2. is_borrow_full() 写在父类 :判断逻辑(已借数量 ≥ 上限)对两种会员完全一样,只有上限数值不同。父类实现逻辑,子类提供数字------共性上提,差异下放,这是继承最实在的样子。

  3. VIPMember.to_dict() 用 super().to_dict() 打底:父类把公共字段拼好,子类只加专属字段。如果重新写一遍全部字段,将来父类加字段就得改两遍。

  4. 密码私有,但又需要存入 JSON :to_dict() 是类内部方法,能读 self.__password。外部只能问 check_password() 对不对,拿不到原文。

系统层:它不知道自己服务几种会员

class LibrarySystem:

def init(self):

self.books: dictstr, Book = {}

self.members: dictstr, Member = {}

def load_data(self) -> None:

... 省略文件检查 ...

for item in self._read_json("members.json"):

多态的应用:靠 type 字段决定实例化哪个子类

if item"type" == "vip":

member: Member = VIPMember(item"card_no", item"password",

item"name", item.get("vip_level", 1))

else:

member = NormalMember(item"card_no", item"password", item"name")

恢复借阅记录,引用真实的图书对象

for book_id in item.get("borrowed_ids", \[\]):

if book_id in self.books:

member.borrowed_books.append(self.booksbook_id)

self.membersmember.get_card_no() = member

def borrow_book(self, member: Member) -> None:

先验上限、再验编号、验库存

if member.is_borrow_full():

print(f"借阅失败:达到最大可借数量({member.get_max_borrow()} 本)")

return

book_id = input("请输入要借阅的图书编号(输0取消):").strip()

target_book = self.books.get(book_id)

if target_book is None:

print("借阅失败:图书编号不存在")

return

if not target_book.is_available():

print(f"借阅失败:《{target_book.title}》暂无库存")

return

target_book.borrow()

member.borrowed_books.append(target_book)

self.save_data()

print(f"借阅成功:您已借 {len(member.borrowed_books)}/{member.get_max_borrow()} 本")

login() 返回 Member | None:卡号或密码错就循环重问,输 0 就返回 None。我觉得这里最优雅的是------"只有登录成功才能访问系统"不是靠一个 is_logged_in 标志位到处判断,而是靠没有 member 对象就根本进不到菜单循环这条结构约束。想绕过都拿不到入场券。

实测:规则全命中

会话 1:普通会员张三(1001 / 123),测试边界规则

  • 借《三体》成功,库存 5 → 4,已借 1/3。

  • 借《唐诗三百首》,库存 0,拒绝。

  • 借《平凡的世界》和《战争与和平》成功,达到 3/3。

  • 再借被拒,提示达到上限。

  • 归还《平凡的世界》,库存 2 → 3。

  • 借《我是猫》(总数 1),成功,该馆尚余 0 本。

会话 2:VIP 王五(9001 / vip888,等级 2)

  • 登录成功,系统提示最多可借 8 本(6 + 2)。

  • 借《三体》成功,提示已借 1/8 本。

  • 同一段菜单代码,张三那里显示 /3,王五这里显示 /8。 系统层没有一行 if 判断会员种类,全是 member.get_max_borrow() 自己返回不同的数。

会话 3:张三重新登录

  • 程序重启,上一次会话已经销毁,借阅记录仍然完整回来了------load_data() 把 borrowed_ids 又还原成了 Book 对象引用,上限依旧是 3/3。

踩到的三个坑

坑一:一行没有注解的 min(),让三处代码一起报错

Book 最初我没写类型注解。self.available = min(available, total) 这一行,因为参数没有类型来源,检查器只能推断出"支持 < 和 > 比较的未知类型"(即 SupportsDunderLT | SupportsDunderGT)。这个非 int 的结论顺着 self.available 传到了所有用到它的地方,导致下游的 > 0、-= 1、min(...) 全部报警。

修法 :不要加抑制注释,而是给源头加类型标注。参数写 total: int,属性写 self.available: int = ...。属性类型一旦确定,下游的告警自然全部消失。

坑二:变量名复用导致的类型冲突

borrow_book 里,我先在展示循环用了 for book in self.books.values(),然后顺手写了 book = self.books.get(book_id)。mypy 立刻报错:循环里的 book 已经被推断为 Book,而 dict.get() 返回的是 Book | None------名字前后的类型不兼容。

修法 :查找结果换个名字 target_book。循环变量用完就别复用,尤其是接下来要接一个可空结果的时候。

坑三:show_borrowed(self, member) 根本不用 self

第 1 篇我就遇到过 PyCharm 的「可降级为静态方法」提示。这两个方法只是"逻辑上属于系统",但不碰任何实例状态,标成 @staticmethod 更诚实。不过加了 @staticmethod 后就不能再用 self,改之前得先把方法体读一遍,确认没用到实例状态。

相关推荐
专业程序开发源1 小时前
springboot欧缇蔓月子中心管理系统82697-计算机课程设计、毕业设计
java·spring boot·后端·python·spring·django·课程设计
FPGA小迷弟2 小时前
用 Claude Code 重构遗留系统:从评估到落地的完整实践指南
c++·python·ai·plotly·django·ai编程·claude
卷无止境2 小时前
面向arm64桌面客户端开发全景:语言、框架与生态
后端·python
薛定e的猫咪2 小时前
(ICLR 2020)MAML 靠的不是快速学习,而是特征复用
人工智能·深度学习·学习·算法·机器学习
fanruitian2 小时前
python else用法
开发语言·python
FRing032 小时前
JavaScript入门到精通二十六(变量提升、函数剩余参数、展开运算符、ES6箭头函数)
开发语言·前端·javascript·es6·箭头函数
云深麋鹿2 小时前
C++ | 基于红黑树实现mymap与myset
开发语言·c++
薛定e的猫咪2 小时前
(ICLR 2026)DF-META:面向 FJSP 的无导数元学习实例自适应调度
人工智能·深度学习·学习·算法·机器学习
Thinker QAQ2 小时前
并发编程(十一):Shared Memory 与 Message Passing——关系与组合使用
java·python·go·并发编程·shared memory·message passing