天之道,损有余而补不足。人之道,损不足以奉有余。
这句话源自《道德经》第七十七章。老子通过观察自然界和社会现象,提出“天道”与“人道”的对立,强调自然规律的公平性与人类社会的不公。
前面这十章,我们一直在拆着学面向对象。
类和对象是什么class 怎么写 实例属性、实例方法怎么理解__init__ 到底干了什么self 到底是谁 类属性、类方法、静态方法有什么区别 封装为什么重要 继承怎么复用代码 多态为什么能让接口统一
如果这些知识点一直分散着看,很多人会有一种感觉:单独一章我都看懂了,但真到自己写项目,脑子里还是有点散,不知道什么时候该用类,什么时候该加方法,什么时候该继承。
所以这一章,我们不再拆知识点,而是做一个完整的小项目,把这一阶段真正串起来。
这次我们做一个很适合入门、又很贴近真实业务的小系统:
图书借阅管理小项目
这个项目不大,但非常适合用面向对象来重构。因为它里面天然就有很多“角色”:
图书 用户 借阅行为 图书馆
而“角色”这两个字,正是面向对象最擅长处理的东西。
这一章你最重要的目标,不是死记代码,而是感受到一件事:
为什么一个稍微真实一点的项目,用类来组织,真的会比一堆零散变量和函数顺很多。
一、先看不用类时,会写成什么样
假设你想做一个最简单的借阅系统。
如果不用类,很多人第一反应可能会写成这样:
book1_name = 'Python 入门'book1_author = '小王'book1_is_borrowed = Falsebook2_name = '数据分析实战'book2_author = '老李'book2_is_borrowed = Falseuser1_name = '张三'user1_borrowed_books = []
然后借书逻辑再单独写函数:
defborrow_book(user_name, borrowed_books, book_name, book_is_borrowed):if book_is_borrowed: print(f'{book_name} 已经被借走了')else: borrowed_books.append(book_name) print(f'{user_name} 成功借阅了 {book_name}')
这时候问题很快就会冒出来。
图书一多,变量会爆炸。 用户一多,也会越来越乱。 图书状态和借阅逻辑是分散的。 谁的数据归谁管,并不清晰。
也就是说,这种写法勉强能跑,但项目一旦稍微变大,代码就会越来越像一堆散落零件。
这正是类最适合出场的时候。
二、先别急着写代码,先识别项目里的对象
做面向对象项目,第一步不是上来就敲 class,而是先问自己:
这个项目里,真正重要的“角色”有哪些
在图书借阅系统里,很容易看出至少有三个核心角色:
图书 用户 图书馆
图书,有书名、作者、是否被借出,还会有借出、归还这些行为。 用户,有姓名、借阅记录,还会有查看借阅清单这种行为。 图书馆,要管理很多本书,也要处理借书、还书、展示图书信息这些动作。
你看,一旦这样去想,面向对象的味道就出来了。
以前你是在想“我要写哪些步骤”。 现在你开始想“这个系统里有哪些角色,各自负责什么”。
这就是典型的对象化思维。
三、第一步,先定义最基础的 Book 类
先从图书对象开始。
一本书最基本应该有什么信息?
书名 作者 借阅状态
它最基础应该会做什么?
展示信息 被借出 被归还
所以我们先写一个 Book 类。
classBook: total_count = 0def__init__(self, title, author): self.title = title self.author = author self._is_borrowed = False Book.total_count += 1defshow_info(self): status = '已借出'if self._is_borrowed else'可借阅' print(f'书名:{self.title},作者:{self.author},状态:{status}')defborrow(self):if self._is_borrowed: print(f'《{self.title}》已经被借走了')returnFalseelse: self._is_borrowed = True print(f'《{self.title}》借阅成功')returnTruedefreturn_book(self):ifnot self._is_borrowed: print(f'《{self.title}》当前并没有被借出')returnFalseelse: self._is_borrowed = False print(f'《{self.title}》归还成功')returnTrue @classmethoddefshow_total_count(cls): print(f'当前系统中一共有 {cls.total_count} 本图书')
这一段代码里,已经把前面好几章的知识点真正串起来了。
__init__ 用来初始化图书对象。self.title、self.author 是实例属性。show_info()、borrow()、return_book() 是实例方法。total_count 是类属性,用来统计总图书数。show_total_count() 是类方法,用来展示整个类层面的共享信息。_is_borrowed 前面加下划线,是在表达:这是内部状态,外部最好不要直接乱改。
你会发现,一旦换成类写,图书这个角色立刻就完整了。
四、Book 类为什么比散乱变量更像“真实对象”
这一步特别值得你感受一下。
以前你可能只有:
book_name = 'Python 入门'book_author = '小王'book_is_borrowed = False
这些变量是散的。 而现在:
一本书的名字 一本书的作者 一本书的借阅状态 一本书该怎么借 一本书该怎么还
全都被组织到 Book 这个对象里了。
这正是封装的力量。
外部不需要知道 _is_borrowed 具体怎么变化。 它只需要通过:
book.borrow()book.return_book()
这些公开入口去操作就行。
这比让外部直接写:
book._is_borrowed = True
要稳得多,也自然得多。
五、第二步,再定义 User 类
接下来轮到用户对象。
一个用户最基本应该有什么?
姓名 借到的书
它又应该会做什么?
查看自己的借阅清单
所以可以写出下面这个类:
classUser:def__init__(self, name): self.name = name self.borrowed_books = []defshow_borrowed_books(self):ifnot self.borrowed_books: print(f'{self.name} 当前还没有借任何书')else: print(f'{self.name} 当前借阅的图书有:')for book in self.borrowed_books: print(f' - 《{book.title}》')
这段代码虽然不长,但已经很清晰了。
用户对象自己保存自己的借阅记录。 用户对象自己知道怎么展示借阅清单。
你会发现,这种写法的最大好处之一就是:
谁的数据归谁管,越来越清楚。
书的数据归 Book 管。 用户的数据归 User 管。 不是所有东西都挤在全局变量里。
六、第三步,定义 Library 类,专门负责“系统管理”
现在图书和用户都有了,但还有一个问题:
借书和还书这种动作,到底由谁来协调
表面上看,借书既和图书有关,也和用户有关。 所以更自然的做法是,再引入一个“图书馆”角色,专门负责整个系统层面的管理工作。
它需要做的事可能包括:
添加图书 展示馆藏 处理借书 处理还书
所以可以定义一个 Library 类。
classLibrary:def__init__(self, name): self.name = name self.books = []defadd_book(self, book): self.books.append(book) print(f'图书馆新增图书:《{book.title}》')defshow_books(self): print(f'欢迎来到 {self.name},当前馆藏如下:')for book in self.books: book.show_info()defborrow_book(self, user, book):if book notin self.books: print(f'《{book.title}》不在当前图书馆馆藏中')returnif book.borrow(): user.borrowed_books.append(book) print(f'{user.name} 已将《{book.title}》加入借阅列表')defreturn_book(self, user, book):if book notin user.borrowed_books: print(f'{user.name} 并没有借阅《{book.title}》')returnif book.return_book(): user.borrowed_books.remove(book) print(f'{user.name} 已完成《{book.title}》归还')
这时候,整个小项目的结构就开始非常清楚了。
Book 管一本书自己的状态。User 管一个用户自己的借阅清单。Library 负责协调图书和用户之间的借阅关系。
这就是面向对象特别擅长的地方:
不是把所有逻辑糊成一团,而是让每个角色各管一摊。
七、完整跑一遍,你会更能体会“结构感”
下面我们把整个系统串起来运行一下。
book1 = Book('Python 入门', '小王')book2 = Book('数据分析实战', '老李')book3 = Book('自动化办公指南', '阿明')user1 = User('张三')user2 = User('李四')library = Library('阳光图书馆')library.add_book(book1)library.add_book(book2)library.add_book(book3)print()library.show_books()print()library.borrow_book(user1, book1)library.borrow_book(user2, book1)library.borrow_book(user2, book2)print()user1.show_borrowed_books()user2.show_borrowed_books()print()library.show_books()print()library.return_book(user1, book1)library.return_book(user2, book3)print()library.show_books()
你不一定非要把输出背下来,但建议你自己脑子里过一遍流程。
图书被加入图书馆 图书馆展示馆藏 用户借书 如果书已经借出,就不能重复借 用户查看自己的借阅记录 用户归还图书 图书馆再展示一次最新状态
这时候你会明显感觉到:
整个项目像一个“小系统”了。 而不是几个孤零零的函数拼在一起。
八、为什么这个项目特别适合用类来重构
因为它里边天然存在多个“角色”。
图书不是一个变量,它是一个独立对象。 用户不是一个字符串,它也是一个独立对象。 图书馆更不是一个函数,它是一个协调多个对象的管理者。
你会发现,越是这种“现实角色很明确”的项目,用类来建模就越自然。
这也是为什么前面我们一直强调:
函数更擅长组织步骤 类更擅长组织角色
到了这个案例里,这句话就完全落地了。
九、现在来加入继承,让结构再进一层
前面这个版本已经能跑,而且已经很像真实小项目了。 但如果你学到这里,就停在“所有图书都一种类型”,那前面的继承和多态就还没真正落地。
现实里,图书馆里的书可能不止一种。
有纸质书 有电子书
它们有共同点:
都有书名 都有作者 都能展示信息
但它们也有差异:
纸质书有页数 电子书有文件大小
这时候,就非常适合用继承来表达。
先把 Book 当父类,然后让 PaperBook 和 EBook 继承它。
classBook: total_count = 0def__init__(self, title, author): self.title = title self.author = author self._is_borrowed = False Book.total_count += 1defshow_info(self): status = '已借出'if self._is_borrowed else'可借阅' print(f'书名:{self.title},作者:{self.author},状态:{status}')defborrow(self):if self._is_borrowed: print(f'《{self.title}》已经被借走了')returnFalseelse: self._is_borrowed = True print(f'《{self.title}》借阅成功')returnTruedefreturn_book(self):ifnot self._is_borrowed: print(f'《{self.title}》当前并没有被借出')returnFalseelse: self._is_borrowed = False print(f'《{self.title}》归还成功')returnTrue
然后定义两个子类:
classPaperBook(Book):def__init__(self, title, author, pages): super().__init__(title, author) self.pages = pagesdefshow_info(self): status = '已借出'if self._is_borrowed else'可借阅' print(f'纸质书:《{self.title}》,作者:{self.author},页数:{self.pages},状态:{status}')classEBook(Book):def__init__(self, title, author, file_size): super().__init__(title, author) self.file_size = file_sizedefshow_info(self): status = '已借出'if self._is_borrowed else'可借阅' print(f'电子书:《{self.title}》,作者:{self.author},文件大小:{self.file_size}MB,状态:{status}')
这里就把继承、super()、方法重写都串起来了。
父类 Book 提供共性。 子类 PaperBook 和 EBook 增加自己的特有属性。 并且重写 show_info(),让同一个接口表现不同效果。
十、这时候,多态就真正落地了
注意 Library.show_books() 这个方法,我们根本不用改。
还是这样写:
defshow_books(self): print(f'欢迎来到 {self.name},当前馆藏如下:')for book in self.books: book.show_info()
为什么不用改?
因为不管 book 是 PaperBook 还是 EBook,它们都支持:
book.show_info()
这就是统一接口。
但不同对象执行后,会打印不同结果。 这就是多态。
我们来试一下:
library = Library('阳光图书馆')book1 = PaperBook('Python 入门', '小王', 320)book2 = EBook('数据分析实战', '老李', 15)book3 = PaperBook('自动化办公指南', '阿明', 280)library.add_book(book1)library.add_book(book2)library.add_book(book3)print()library.show_books()
这时候,外部只知道统一调用 show_info()。 至于有的显示页数,有的显示文件大小,那是不同子类自己决定的。
你会发现,这就是前一章讲的多态,在真实项目里非常自然的使用方式。
十一、封装在这个项目里又体现在哪里
很多人学封装的时候,容易停留在“哦,前面加下划线”这个层面。 但这个项目刚好能帮你把封装真正看明白。
比如图书的借阅状态 _is_borrowed。 外部理论上可以直接改,但正常设计下,最好不要这样干。 而应该通过:
book.borrow()book.return_book()
这些方法来改变状态。
这样做的意义在于:
借阅成功还是失败 归还是否合理 状态如何变更
这些规则都由对象自己控制。
这就是封装的价值:
不是暴露内部状态让外部乱改,而是提供合理入口让外部正常操作。
十二、为什么这个项目比过程式写法更容易扩展
这一步特别重要。
如果你一开始是过程式硬写,后来想加“电子书”这种新类型,往往会非常麻烦。因为你很多地方可能都写死了对某种图书结构的判断。
但现在用了类以后,扩展就顺很多。
想加新类型?
新建一个子类就行。
想让某类图书展示方式不同?
重写 show_info() 就行。
想让某类图书借阅规则不一样?
重写 borrow() 就行。
比如以后你还可以写一个“参考书”子类,规定它只能馆内阅读,不能借出:
classReferenceBook(Book):defborrow(self): print(f'《{self.title}》属于参考书,不能外借')returnFalse
你看,只加一个类,整个系统就获得了新能力。 而图书馆里统一的借书流程几乎不用大改。
这就是面向对象在项目扩展上的优势。
十三、类属性在这个项目里也不是摆设
前面 Book.total_count 看起来像一个顺手加的计数器,但它其实非常实用。
比如你可以直接这样看总图书数:
Book.show_total_count()
或者补一个类方法:
@classmethoddefshow_total_count(cls): print(f'当前系统中一共有 {cls.total_count} 本图书')
这时候你会更明显理解:
类属性不是某本书自己的信息 而是整个图书类共享的信息
这一章把它放进项目里,就是为了让你真正看到它的用武之地。 不是为了考试记名词,而是为了项目里真的能用上。
十四、静态方法也可以很自然地放进来
前面说过,静态方法最适合做那种“和类有关,但不依赖实例状态,也不依赖类状态”的工具逻辑。
在这个项目里,也很容易找到这样的场景。
比如判断书名是否合法:
classBook: total_count = 0def__init__(self, title, author): self.title = title self.author = author self._is_borrowed = False Book.total_count += 1 @staticmethoddefis_valid_title(title):return len(title.strip()) > 0
调用时:
print(Book.is_valid_title('Python 入门'))print(Book.is_valid_title(' '))
你会发现,这个功能和 Book 主题有关,但它既不需要某本具体书的数据,也不需要整个类共享信息。 这就很适合写成静态方法。
这样一来,类属性、类方法、静态方法,就不是零散知识点,而是都在项目里找到位置了。
十五、这个项目真正锻炼你的,不只是 OOP 语法
说到底,这一章最重要的,不只是让你把几个类写出来,而是训练你一种更接近真实开发的思路:
先识别角色 再明确每个角色该负责什么 把共性抽出来 把个性留给子类 把内部状态保护起来 让外部通过统一接口来用
这套思路,你以后做学生管理系统、订单系统、用户系统、记账系统,其实都能用上。
所以这一章虽然写的是图书借阅,但它练的不是“图书”本身,而是“怎么用类去组织一个小项目”。
这才是真正值钱的地方。
十六、再给你一个更完整的整合版代码
下面给你一个更完整、适合自己敲一遍的整合版。
classBook: total_count = 0def__init__(self, title, author): self.title = title self.author = author self._is_borrowed = False Book.total_count += 1defshow_info(self): status = '已借出'if self._is_borrowed else'可借阅' print(f'书名:{self.title},作者:{self.author},状态:{status}')defborrow(self):if self._is_borrowed: print(f'《{self.title}》已经被借走了')returnFalse self._is_borrowed = True print(f'《{self.title}》借阅成功')returnTruedefreturn_book(self):ifnot self._is_borrowed: print(f'《{self.title}》当前并没有被借出')returnFalse self._is_borrowed = False print(f'《{self.title}》归还成功')returnTrue @classmethoddefshow_total_count(cls): print(f'当前系统中一共有 {cls.total_count} 本图书') @staticmethoddefis_valid_title(title):return len(title.strip()) > 0classPaperBook(Book):def__init__(self, title, author, pages): super().__init__(title, author) self.pages = pagesdefshow_info(self): status = '已借出'if self._is_borrowed else'可借阅' print(f'纸质书:《{self.title}》,作者:{self.author},页数:{self.pages},状态:{status}')classEBook(Book):def__init__(self, title, author, file_size): super().__init__(title, author) self.file_size = file_sizedefshow_info(self): status = '已借出'if self._is_borrowed else'可借阅' print(f'电子书:《{self.title}》,作者:{self.author},文件大小:{self.file_size}MB,状态:{status}')classUser:def__init__(self, name): self.name = name self.borrowed_books = []defshow_borrowed_books(self):ifnot self.borrowed_books: print(f'{self.name} 当前还没有借任何书')else: print(f'{self.name} 当前借阅的图书有:')for book in self.borrowed_books: print(f' - 《{book.title}》')classLibrary:def__init__(self, name): self.name = name self.books = []defadd_book(self, book): self.books.append(book) print(f'图书馆新增图书:《{book.title}》')defshow_books(self): print(f'欢迎来到 {self.name},当前馆藏如下:')for book in self.books: book.show_info()defborrow_book(self, user, book):if book notin self.books: print(f'《{book.title}》不在当前图书馆馆藏中')returnif book.borrow(): user.borrowed_books.append(book) print(f'{user.name} 已将《{book.title}》加入借阅列表')defreturn_book(self, user, book):if book notin user.borrowed_books: print(f'{user.name} 并没有借阅《{book.title}》')returnif book.return_book(): user.borrowed_books.remove(book) print(f'{user.name} 已完成《{book.title}》归还')
这套代码并不算长,但它已经很像一个完整的小项目骨架了。
十七、你现在应该重点看懂什么
不是每一行都背下来。 而是重点看懂下面这些关系:
Book 为什么是父类PaperBook 和 EBook 为什么适合继承它show_info() 为什么能体现多态_is_borrowed 为什么体现了封装Library 为什么不适合只是一个普通函数集合User 为什么也应该是独立对象
只要这些关系看明白了,你就不是在机械抄代码,而是真的开始有项目建模能力了。
十八、做完这一章,你会发现面向对象终于“落地”了
前面十章,其实一直都像在搭骨架。
第71章到第75章,是在搭基础骨架。 第76章到第79章,是在补更完整的结构能力。 而这一章,就是把前面的骨架真正装进一个小系统里。
你会突然发现,那些原本看起来有点抽象的词:
类 对象 封装 继承 多态
到了项目里以后,其实都很具体。
不是为了定义而定义。 不是为了显得高级而高级。 而是真的能让代码更整洁、更清楚、更容易扩展。
这就是为什么学面向对象,不能只停留在背概念。 一定要进到项目里,才会真的通。
十九、本章小练习
你可以基于这一章的图书借阅系统,自己做两个升级版练习。
第一个练习,加入“借阅上限”功能。 比如每个用户最多只能借 3 本书。 这会迫使你去思考:这个规则应该放在 User 里,还是放在 Library.borrow_book() 里。
第二个练习,加入“参考书不能外借”功能。 写一个 ReferenceBook 子类,重写 borrow() 方法。 这会帮助你真正把继承和多态用起来。
如果你能自己把这两个功能补进去,这一阶段的内容就算真的吃透了。
二十、本章总结
这一章,我们没有再拆零碎知识点,而是把面向对象这一阶段真正收束成了一个完整的小项目。
你看到了怎么从真实业务里识别对象。 看到了如何用类来组织图书、用户、图书馆这些角色。 看到了封装如何保护内部状态。 看到了继承如何提取共性、减少重复。 看到了多态如何让统一接口在不同对象上表现不同。 也看到了类属性、类方法、静态方法在项目里到底能放在哪里。
到这里,第八阶段就算真正收束了。 你已经不只是会写几个类了,而是开始具备用类去组织一个小型真实项目的能力。
下一章,我们正式进入第九阶段:081|模块是什么:为什么 Python 代码可以拆文件。