Python面向对象编程入门:从类与对象到可维护代码
简介:本资源是《Python Object-Oriented Programming》第四版的英文原版电子书,由 Steven F. Lott 与 Dusty Phillips 合著,Packt Publishing 于 2021 年出版。该书面向已具备 Python 语法基础、希望系统掌握面向对象设计并构建可维护应用与库的开发者,也可作为高校相关课程的参考读物。全书围绕封装、继承、多态三大机制展开,从类与实例、属性与方法等基础概念讲起,逐步深入模块化组织、异常处理、单元测试与文档编写等工程实践,并专门讨论版本控制、依赖管理、持续集成与社区协作等库维护要点,配有大量可直接借鉴的代码范例。资源包共 1 个文件,为 PDF 格式,体积约 6.51MB,单册完整收录正文内容,便于在电脑或平板上检索与批注。目前已有 43 人学习关注。读者可从中获得从语法到架构的完整 OOP 知识路径,理解如何用类层次组织复杂业务、用测试与异常机制提升健壮性,并参照书中范例重构自己的项目代码。
1. 先说清楚:这本书到底在教你什么
很多人学 Python 卡住的地方不是语法,而是代码一多就乱成一锅粥:变量满天飞,改一处崩三处。《Python 面向对象编程(第四版)》要解决的就是这件事——教你把数据和操作数据的方法打包成一个个“对象”,让程序从一堆零散的函数,变成一群各司其职、能自己干活的小模块。书里从类和对象讲起,一路覆盖继承与组合、异常处理、内置数据结构、常用设计模式,最后落到自动化测试和并发编程。它适合已经能写循环和函数、但一遇到多文件项目就发怵的人,也适合从别的语言转过来、想看清 Python 独有写法的人。完全零基础也不用怕,前六章几乎是重新讲“什么是对象”。
2. 为什么新手也该先搞懂面向对象:从“能跑”到“能改”
2.1 函数堆成山的那一刻,就是该学面向对象的时候
先别急着翻书,回想一下自己写过的脚本。最开始可能是三十行:读一个表格,算几个数,打印结果。两个月后老板说再加几个字段,你又加了二十行。半年后这个文件变成八百行,里面有十几个名字含糊的函数,几个全局变量在函数之间来回传递,还有一个变量在三个地方被修改,你根本不知道哪一次修改是“最新”的。
这时候你打开这个文件,会有一个很具体的感受:不敢改。因为任何一处改动都可能牵连别处,而你没有精力把整条链路在脑子里跑一遍。这种恐惧不是能力问题,是代码结构的问题。函数式写法把“数据”和“操作”拆开了,数据在函数之间传递,操作散落在各处,程序越大,这种分离带来的成本就越高。
面向对象给出的解法很朴素:把“跟着同一份数据转的那批操作”打包在一起,让数据和操作住进同一个盒子。这样你改这个盒子内部的时候,只要盒子对外的口子不变,外面就不用动。这就是可维护性的具体含义,不是什么抽象的大词。
《Python 面向对象编程(第四版)》整本书都在讲这件事的不同侧面:怎么打包(类和对象)、怎么复用(继承和组合)、怎么让错误可控(异常)、怎么让代码结构更稳(设计模式)、怎么确认自己没改坏(测试)。前六章是地基,不懂地基直接看后面,翻车概率极高——这是很多人的血泪经验。
2.2 类和对象到底是什么:用“奶茶店点单”讲明白
官方定义通常是“类是对象的蓝图,对象是类的实例”。这句话没错,但它对新手基本等于没说。换个比喻。
你走进一家奶茶店,墙上有一张菜单,写着“珍珠奶茶,可选杯型、甜度、冰量,价格 = 基础价 + 加料价”。这张菜单就是类:它规定了这类东西长什么样、能做什么,但它不能喝。
你点了一杯“中杯、三分糖、少冰、加珍珠,16 元”。这张具体的订单就是对象(也叫实例):它有自己具体的状态,是可以真正拿来用的东西。
订单上“三分糖”“中杯”这些信息,就是属性,描述这个对象现在什么样。订单上“可以加料”“可以算总价”这些能力,就是方法,描述这个对象能做什么动作。
再用程序的话说一遍:
- 类,是你写的一段代码,定义了一组属性和方法;
- 对象,是运行时真正创建出来的、有具体数据的那一个;
- 属性,就是这个对象身上挂着的变量,比如
self.price; - 方法,就是这个对象能调用的函数,比如
line.subtotal()。
为什么这套东西有威力?因为“每天有三千杯奶茶”的时候,你不需要写三千份菜单,只写一份类,然后创建三千个对象,每个对象各自记自己的甜度。数据被分开了,而处理逻辑只写了一遍。这是新手最容易立刻感受到的好处。
还有两个常被提起的词,顺便用这个比喻带过去。封装,就是顾客不需要知道后厨怎么煮珍珠,只面对菜单点单;代码上就是外部只调用方法,不直接翻看内部变量。继承,就是“果茶菜单可以复用奶茶菜单里‘杯型、甜度、冰量’这些通用部分,再加自己的水果配置”;代码上就是子类拿父类现成的东西,只写差异。这两个概念第 3 章会动手写出来,先有个印象就够。
提示:一开始别纠结“我的类设计得对不对”。新手阶段,能写出一个把数据和动作装在一起的类,就已经跨过门槛了,设计好不好是练出来的。
2.3 这本书的路线图:四段式,从概念到能上线的代码
这本书一共十四章,结构其实很清楚,分成四段。新手如果不清楚这个结构,很容易第一章看两页就弃,觉得“怎么全是概念”。
第一段是第 1 到第 6 章,核心概念。第 1 章讲对象、属性、行为、封装、继承、组合这些词到底指什么;第 2 章真正开始写 Python 类,同时引入类型提示(type hints)、模块和包;第 3 章讲类之间的相似关系,也就是继承、多继承、多态;第 4 章讲异常处理,怎么让程序在数据脏的时候不直接崩;第 5 章回答一个很实际的问题——什么时候该用面向对象,什么时候不该;第 6 章讲抽象基类和运算符重载,属于进阶但非常提气的部分。
第二段是第 7 到第 9 章,回头把 Python 自带的家伙什儿用面向对象的视角重新看一遍:元组、命名元组、dataclass、字典、列表、集合、队列,然后是字符串、正则、文件路径、JSON 序列化。这一段特别适合新手,因为它不讲新概念,而是把你天天用的东西讲透,你会频繁产生“原来是这么回事”的感觉。
第三段是第 10 到第 12 章,设计模式。迭代器模式、装饰器、观察者、策略、命令、状态、单例,再往后是适配器、外观、享元、抽象工厂、组合、模板。这里的关键是别背名字,要理解“这个模式解决的是哪一类反复出现的问题”。
第四段是第 13、14 章,测试与并发。用 unittest 和 pytest 写自动化测试,然后是线程、多进程、futures、异步 IO。
贯穿全书还有一个案例:一个简单的手写数字识别式的分类器,用的是“找最相似的几个邻居来投票”的思路。它不是玩具,每章都会在这个案例上新增一层功能,你跟着走完,会看到一个程序怎么从几行代码长成一个有结构的小系统。这种一以贯之的案例,对新手理解“为什么要有这些结构”帮助极大,比看十个互相独立的示例强。
3. 第一步怎么迈:跟着写第一个能跑的类
3.1 准备环境:三样东西就够了
不要被“环境配置”这四个字吓到,入门阶段你只需要三样:
第一,一个 Python 解释器。装 3.9 或更新的版本都行,这本书的示例用到类型提示,版本太老会报错。
第二,一个能写代码的编辑器,用你顺手的就行。
第三,一个干净的虚拟环境。这一步很多新手跳过,然后过两个月发现装了一堆库互相打架,后悔药都没得吃。在命令行里执行:
# 在项目文件夹下创建一个独立的 Python 环境 python -m venv .venv # 激活它(Windows) .venv\Scripts\activate # 激活它(macOS / Linux) source .venv/bin/activatevenv是 Python 自带的模块,不需要额外安装。.venv是给这个环境起的名字,你可以随便改;激活之后,命令行前面会出现(.venv),说明你后续装的库只会装进这个文件夹里,不会污染系统里的 Python。项目不要了,把这个文件夹删掉就干净了。
注意:每次打开新的命令行窗口,都要重新激活一次虚拟环境,这是新手最常忘记的一步,忘了就会出现“我明明装了库怎么还报 ModuleNotFoundError”。
3.2 第一个类:把数据和动作装进同一个盒子
我们从奶茶店那个比喻直接落地。新建一个文件order.py,写第一个类:
# order.py # 订单里的一行:记录商品名、单价、数量,并能算出小计 class OrderLine: def __init__(self, name: str, price: float, quantity: int = 1): # __init__ 是创建对象时自动执行的方法,用来把初始数据挂到对象上 self.name = name # 属性:商品名 self.price = price # 属性:单价 self.quantity = quantity # 属性:数量,默认 1 def subtotal(self) -> float: # 方法:算这一行的小计 return self.price * self.quantity if __name__ == "__main__": line = OrderLine("笔记本", 12.5, 3) print(line.name, line.subtotal())一段一段看。class OrderLine:就是定义了一个类,名字习惯用大驼峰。__init__是初始化方法,创建对象的时候自动跑一次,它的第一个参数self指的是“正在被创建的这个对象本身”。self.name = name的意思是:把传进来的参数name,挂到这个对象身上,以后通过line.name就能取到。这就是属性的诞生过程。
def subtotal(self)是方法。注意它也必须带self,因为它要读这个对象自己的price和quantity。不带self的话,这个方法不知道自己算的是哪一行的账。
参数说明:name: str里的: str是类型提示,它不影响运行,只是告诉读代码的人和你用的编辑器“这里应该是个字符串”。quantity: int = 1里的= 1是默认值,创建对象时不传数量就按 1 算。类型提示是这本书第 2 章重点讲的特性,养成写它的习惯,能少踩很多坑。
跑一下python order.py,输出笔记本 37.5。到这里,你已经完成了从“函数”到“对象”的第一次跨越:数据和算这个数据的动作,现在住在一起了。
再往前推一步,用dataclass写同一件事,代码会短很多:
from dataclasses import dataclass @dataclass class OrderLine: name: str price: float quantity: int = 1 def subtotal(self) -> float: return self.price * self.quantity@dataclass是一个装饰器,它会自动帮你生成__init__、__repr__这些样板方法。你只需要写字段和方法,重复代码被干掉了。这本书第 7 章专门讲这个,新手提前看到不用怕,理解成“Python 帮你写好了__init__”就行。什么时候用普通类、什么时候用 dataclass,书里有明确的判断标准,别自己拍脑袋。
3.3 第二个例子:继承与组合,让代码不用重复抄
单个类不够用。现实里“订单”是由“多个行”组成的,这层关系用组合表达:一个对象里面装着另一些对象。
# 在 order.py 里接着写 class Order: def __init__(self, lines=None): # lines 是可选的;不传就创建一个空列表,注意不要写成 lines=[] self.lines = list(lines or []) def add(self, line: OrderLine) -> "Order": self.lines.append(line) return self # 返回自己,方便链式调用 def total(self) -> float: # 把每一行的小计加起来 return sum(line.subtotal() for line in self.lines)这里有个新手必踩的坑:def __init__(self, lines=[])。这样写,所有对象会共享同一个列表,你往 A 订单里加商品,B 订单也莫名其妙多了一件。所以正确写法是先写None,再在函数体里创建新的列表。return self是为了支持Order().add(a).add(b)这种连续调用,不用也无所谓,属于风格问题。
跑一下:
order = Order() order.add(OrderLine("笔记本", 12.5, 3)).add(OrderLine("钢笔", 30, 1)) print(order.total()) # 67.5接下来看继承。假设店里搞活动,某些商品打八折:
class DiscountedLine(OrderLine): def __init__(self, name: str, price: float, quantity: int = 1, rate: float = 0.8): super().__init__(name, price, quantity) # 先让父类把通用字段处理好 self.rate = rate # 再补上子类独有的字段 def subtotal(self) -> float: # 覆盖父类的同名方法,加上折扣 return super().subtotal() * self.rateclass DiscountedLine(OrderLine)表示“打折行是一种订单行”,括号里是父类。super().__init__(...)调用父类的初始化,把name、price、quantity交给父类处理,避免重写一遍。subtotal方法同名重写,这叫覆盖,它的好处是外部代码完全不用改:order.add(DiscountedLine(...))之后,order.total()自动按折扣算,因为total里调的是每一行自己的subtotal()。这就是多态的实际价值——调用方不需要知道具体是哪一类行,只要它会算小计就行。
新手容易在这里迷失:继承不是“看起来像就能继承”。判断标准只有一条,子类是不是父类的一种特殊版本。笔记本打折行是订单行,成立。而“订单”和“订单行”之间不是继承,是组合。这个区分想不清楚,代码结构很快就会变形,第 3 章和第 5 章来回讲的就是这个判断。
3.4 跑完之后自检:三个问题确认你真的懂了
代码跑通不代表学会。第一个例子跑完之后,停下来问自己三个问题。
第一个:如果我要给每一行加一个“备注”字段,需要改几个地方?答案是最多改两处——init里加一行赋值,type hint 那里加个类型标注,其他地方不用动。这就是封装带来的好处:变化被限制在一个盒子里。
第二个:如果我删掉Order.total方法,改成在外面写一个函数sum(order.lines),行不行?行,这也不是错。第 5 章讨论的就是“什么时候值得写成类”,如果一个类只有一个方法、只被调用一次,那确实不如直接写函数。面向对象不是越用越好,是“该用的时候用”。
第三个:如果quantity传进来是负数,会发生什么?答案是算出来的小计也是负数,程序一声不吭地跑完了,然后你在报表里看到一堆负数。这就是第 4 章异常处理的入口:数据不合规的时候,程序应该主动喊停,而不是默默算出一个错数字。
这三个问题如果你都能答上来,说明你已经站在面向对象的门口了。这本书后面的所有内容,都是在这三扇门后面继续走。
4. 新手最容易翻车的五个坑:排查清单
4.1 坑一:以为“用了类”就是面向对象
现象:写了十几个类,每个类里除了__init__以外什么都没有,全部逻辑还是堆在外面几个大函数里,最后代码比不用类的时候还长,绕得更晕。
原因:把类当成了“数据容器”来用,只放属性不放方法,等于把数据打包了一层壳,但没有把“跟着数据转的那批逻辑”一起搬进去。原本散在函数里的判断和计算,现在散在函数和类之间,反而更乱。
解决:判断一个类该不该有某个方法,看它动不动这组数据。如果一段逻辑全是围绕line.price和line.quantity在做运算,那它就该是line的方法。改完之后,外部代码从“取数据、算、塞回去”变成“让对象自己算”,这才是面向对象省下来的那部分力气。
4.2 坑二:self忘了写,或者把方法写成了模块函数
现象:调用line.subtotal()的时候报TypeError: subtotal() takes 0 positional arguments but 1 was given,明明参数都写对了。
原因:方法定义时漏了self。Python 在调用实例方法时,会自动把对象本身作为第一个参数传进去,所以定义时必须留出这个位置。def subtotal():这种写法,Python 会认为它不接受参数,结果你通过对象调用时多塞了一个,就报错。
解决:实例方法的第一个参数固定写self,并且第一个参数名可以改,但不要改,改了别人读代码会困惑。另外,如果你发现某个方法压根用不到self,也不该写成def foo(self)塞在那里,把它改成模块级函数,或者在书上第 5 章讲的场景里用@staticmethod,这才是干净的做法。
4.3 坑三:为了让子类复用代码,硬套继承
现象:写着写着,Manager继承了Employee,Employee又继承了Person,然后为了复用某个字段,又让Order继承了Product。最后调一个方法,得往上翻四五层父类才能找到它到底在哪定义,改一处崩一片。
原因:把继承当成了“复制代码的快捷方式”。继承的本意是表达“是一种”的关系,不是“我需要这两个字段”。用它来共享代码,层级会越来越深,而深继承是最难维护的结构之一。
解决:能用组合就别用继承。订单包含若干订单行,用组合;打折行是一种订单行,用继承。分辨方法是造个句子:“A 是一种 B”读起来通顺,才考虑继承。读起来别扭,就换成“A 里有一个 B”,用组合。这本书第 3 章给了很多这样的对照例子,值得反复看。
提示:继承层级不要超过两层。超过两层,说明你大概率在用它偷懒,不是在用它建模。
4.4 坑四:异常捕获写成“一把梭”
现象:程序里到处都是try: ... except Exception: pass。看起来不报错了,但实际上数据错了你也不知道,等到下游发现数字全不对的时候,已经过去两个星期,查都查不出来。
原因:怕报错,所以把所有异常一口吞掉。except Exception会捕获包括拼写错误、类型错误在内的一切问题,pass又不做任何处理,等于把错误从“当场炸”变成“以后炸”。
解决:只捕获你知道怎么处理的那一类异常,其余的让它往上冒。书里第 4 章讲的做法,是给业务问题定义一个自己的异常类,比如InvalidQuantity,然后在数据校验的地方主动raise。这样报错信息一眼就能看出是业务规则被违反,而不是一串看不懂的堆栈。捕获的时候写except InvalidQuantity as e:,在异常里带上具体数值,事后排查会轻松很多。
class InvalidQuantity(ValueError): """数量必须为正整数时抛出""" pass def check_quantity(q: int) -> None: if q <= 0: raise InvalidQuantity(f"数量必须是正整数,收到 {q}")这个类只做一件事,但它的名字本身就把“哪里出问题”讲清楚了。新手往往觉得自定义异常是多余的,改上两三次代码之后就会反过来感谢它。
4.5 坑五:不写测试,改一行崩三处
现象:加了一个小功能,跑了一遍看起来正常,就交上去了。两天后接到反馈,说三个月前做的一个报表页面数据错了,你的改动把那条路径弄坏了,而这条路径你根本不知道存在。
原因:手动点几下只能覆盖你想到的路径,而程序里的路径比你想到的多得多。没有自动化测试,你每次改动都是在赌博。
解决:从最小的测试写起,一个判断就够。第 13 章讲 pytest,写法极简:一个test_开头的函数,里面一个assert。你不需要一次覆盖全部,先把最容易出错的三个计算点写死,以后每改一次代码就跑一遍。这件事的前期成本大概半小时,省下来的排查时间是一个数量级。
# test_order.py from order import Order, OrderLine, DiscountedLine def test_total_with_plain_lines(): order = Order([OrderLine("笔记本", 12.5, 3), OrderLine("钢笔", 30, 1)]) assert order.total() == 67.5 def test_discount_applies(): order = Order([DiscountedLine("笔记本", 12.5, 2)]) assert order.total() == 20.0运行pytest,两个通过就是绿灯。参数说明:pytest会自动发现当前目录下test_开头的文件和函数,不需要你手动注册。断言失败时它会打印实际值和期望值,看那个数字就知道哪里算错了。
5. 学完核心章节之后:下一步往哪走
很多人卡在这里:前六章看完了,后面九章每一章都像新世界,不知道先扑哪个。我踩过的坑是按顺序硬啃设计模式,结果看完只会背名字,遇到问题还是写不出来。后来调整了顺序,效率完全不一样。
建议的顺序是这样的:先把第 7 章的数据结构吃透,因为元组、dataclass、字典、集合这些是你每天都在用的东西,学完立刻能用上,正反馈最快。然后跳到第 13 章学测试,越早越好,测试是后面所有改动的安全网,没有它,你学设计模式的时候不敢动自己的代码。再回头学第 10 章的迭代器与生成器,这一章是 Python 里最实用的一招,处理大文件、分批读数据都靠它。最后才是第 11、12 章的模式,那时候你已经有了自己的项目痛点,看到某个模式会立刻对上号,而不是空转。
时间安排上,每周一个主题比较现实:
| 周次 | 主题 | 对应章节 | 每周产出 |
|---|---|---|---|
| 第 1 周 | 类与对象、属性与方法 | 1、2 | 一个能跑的小工具类 |
| 第 2 周 | 继承、组合、多态 | 3 | 把上一步的工具类重构一次 |
| 第 3 周 | 异常与数据校验 | 4、5 | 给工具类加上输入检查 |
| 第 4 周 | 内置数据结构 | 7 | 用 dataclass 重写数据部分 |
| 第 5 周 | 自动化测试 | 13 | 三个以上的测试用例 |
| 第 6 周 | 迭代器与生成器 | 10 | 改成逐行处理,不再一次读入内存 |
这个表的意义不在于严格照做,而在于给你一个“每周都有东西能跑出来”的节奏。看书最怕的就是连续两周没有产出,一旦断了,就很难接上。
最后分享一个我自己的习惯,也是踩了太多坑之后养成的:每次想加一个新功能之前,先给现有代码补一个测试,看着它变绿,再动手改。这个顺序听起来多余,但它把“我改坏了没有”这个问题,从“凭感觉”变成了“看颜色”。从那以后我每次重构自己的小项目,都强制走一遍“先跑测试、再改代码、改完再跑一遍”的流程,一次都没省过。省下来的排查时间,比写测试的时间多得多。
入门这件事,最难的不是概念本身,是迈出第一脚。从OrderLine那个二十行的类开始写,写完你会发现,面向对象没那么玄。希望帮到你。
本文还有配套的精品资源,点击获取