资讯详情

Python面向对象编程入门:类、继承、组合与pytest实战

📅 2026/11/2 13:49:17 | 华诺云谱 👁 阅读
Python面向对象编程入门:类、继承、组合与pytest实战
简介《Python Object-Oriented Programming》第四版电子书面向已掌握Python基础语法、希望系统提升面向对象设计与工程化能力的开发者帮助其构建健壮、可维护的应用与库。全书从封装、继承、多态等核心机制讲起逐步深入属性管理、方法解析顺序、抽象基类、描述符与元类等进阶话题并延伸至模块化组织、异常处理、单元测试、文档编写以及版本控制、依赖管理与持续集成等库维护实践兼顾设计思路与落地细节。压缩包内共1个PDF文件约6.51MB为Packt于2021年出版的英文原版第四版保留完整目录与章节结构便于按主题检索与对照阅读目前已有45人学习。读者可将其作为案头参考既能系统梳理OOP概念体系也能借助书中示例体会类设计、接口抽象与测试驱动改进的具体做法适合需要在项目中重构代码、沉淀可复用组件的中级开发者反复查阅。1. 先搞清楚一件事Python 面向对象编程到底在解决什么新手第一次听到「Python 面向对象编程」脑子里冒出来的往往是一串看不懂的术语类、实例、封装、继承、多态。我第一次翻这类书的时候看了三页就把书合上了——明明十几行脚本就能跑起来的事为什么非要绕这么大一圈这本书第四版给出的答案其实很朴素当代码从几十行涨到几千行改一个小功能会牵动五个文件那种「不敢动、一动就崩」的恐惧感就是面向对象要解决的问题。它把数据和操作数据的方法打包成一个个对象像把散在桌上的零件装进贴着标签的盒子找起来快换起来也不容易碰坏别的东西。这本书适合已经会写基础 Python、但一写稍大的项目就乱成一团的人。2. 第一个类怎么写从「一堆字典」到「一个有脾气的对象」学任何东西最怕的是停在概念上打转。这一章不讲设计哲学只做一件事让你在终端里亲手敲出一个类看着它跑起来然后明白刚才敲的每一行到底在干什么。书里第二章叫「Python 中的对象」讲的就是从语法层面把类、属性、方法、模块这几样东西立起来。我们照着这个顺序走但把步子拆得更碎一点。2.1 为什么新手应该先扔掉「字典满天飞」的写法在学类之前绝大多数人的代码长这样一个字典装数据一堆函数围着这个字典转。写十个八个脚本没问题一旦功能涨起来问题就冒头了。你不知道这个字典里到底有哪些键、哪个函数会偷偷往里塞东西、改一个键名要全项目搜索替换。这不是玄学是血泪经验——我见过一个项目里同一个字典在七个文件里被改来改去最后没人说得清它到底长什么样。类解决的正是这件事。它把「有哪些数据」和「能对这些数据做什么」写在同一张纸上形成一份契约。你打开这个类的定义三秒钟就能知道它有什么、能干什么。书里反复强调的封装说白了就是别让外面的人随便伸手进来改你的东西要用就通过你留的门。这个门叫公开接口门里面的东西叫实现细节。还有一层更实际的好处类型提示。这本书从第二章就开始讲 type hints这在 Python 里不是可有可无的装饰。当你在函数签名里写上name: str、price: float编辑器会自动给你补全写错了会当场标红。新手最常犯的低级错误——把字符串当数字算——有一大半能被这行注释提前拦住。至于模块和包先记住一句话就够一个.py文件就是一个模块一个文件夹里面有__init__.py就是一个包。当你的类多起来把它们按职责分到不同文件里别全堆在一个main.py里。书里专门用一节讲绝对导入和相对导入新手阶段记住「优先用绝对导入」就不会错。2.2 跑通第一个类八行代码看清属性、方法和初始化先建一个文件叫coffee.py。下面这段代码可以直接抄进编辑器一个字都不用改。# coffee.py class Coffee: 一杯咖啡把名字和价格绑在一起省得成天传两个变量 def __init__(self, name: str, price: float) - None: # self 就是这一杯本身值挂在它身上别的杯子不受影响 self.name: str name self.price: float price def describe(self) - str: # 方法就是挂在对象身上的函数能直接读到自己的属性 return f{self.name}{self.price:.1f} 元逐行拆开看。class Coffee:是画图纸此刻还没造出任何一杯咖啡。__init__是一个特殊方法创建实例的那一刻自动执行你不需要手动调用它。它的第一个参数永远是self代表「正在被创建的这个对象」后面两个参数才是你真正要传进去的数据。self.name name这一行是关键左边带self的是挂在这个对象身上的属性右边不带self的是刚才传进来的参数。两者同名但完全不是一回事新手在这一行上卡住的不在少数。describe是普通方法它做的事情是读自己的属性、拼一句话返回没有副作用也不修改任何东西。再建一个文件demo.py和coffee.py放在同一个目录下# demo.py from coffee import Coffee mocha Coffee(摩卡, 28.0) # 造一个实例 latte Coffee(拿铁, 25.0) # 再造一个两者互不干扰 print(mocha.describe()) # 摩卡28.0 元 print(latte.describe()) # 拿铁25.0 元 mocha.price 30.0 # 改一个对象的属性 print(mocha.describe()) # 摩卡30.0 元 print(latte.describe()) # 拿铁25.0 元完全没变在终端里cd到这两个文件所在的目录执行python demo.py。看到四行输出你的第一个类就跑通了。最后两行输出值得多看一眼改mocha的价格latte纹丝不动。这就是实例属性——每个对象各有一份。如果哪天你发现改了一个对象另一个跟着变了那八成是踩了类属性的坑第四章会专门讲这件事。提示文件名别叫coffee.py又去import coffee如果你在同一个目录下还有个叫coffee的包导入会打架。新手阶段给文件起名尽量具体一点比如drink_model.py。2.3 把类型注解、docstring 和 self 一次讲透上面那段代码里有三个东西新手容易糊弄过去但它们恰恰是这本书反复强调的部分。第一个是类型注解。name: str只是标注Python 运行时不会强制检查你传个数字进去照样能跑。那标注有什么用它让编辑器和静态检查工具能提前发现错误也让三个月后的你看懂这个参数本来该是什么。书里第二章花了不小篇幅讲类型检查和mypy这类工具入门的用法很简单装一个编辑器插件让它在后台帮你盯着。第二个是 docstring就是三个引号包起来的那行说明。它不是给人看的注释而是可以被help()读出来的正式文档。你写help(Coffee)就能看到它。养成给类和一两个关键方法写一句 docstring 的习惯成本极低回报很高。第三个是self到底要不要写。Python 和你可能听过的其他语言不一样它要求你在定义方法时显式写上self。漏写它的典型报错是TypeError: describe() takes 0 positional arguments but 1 was given——明明参数对得上为什么说多给了一个因为调用mocha.describe()时Python 自动把mocha本身当作第一个参数塞了进去。看到这个报错第一反应就该是去数一下方法定义里有没有self。到这里你已经把「类是什么、实例是什么、属性怎么存、方法怎么写」这四件事落地了。接下来该处理一个更现实的问题当你手里有咖啡、有茶、有果汁它们都有名字和价格难道要写三遍几乎一样的代码3. 类与类的关系继承、组合和「什么时候别继承」书里第三章的标题叫「当对象彼此相似时」这个说法很准确。继承不是炫技它出现的原因非常朴素你发现三个类里有五分之四的代码是一模一样的。这一章要解决的就是怎么把重复的部分提上去、把不同的部分留下来以及一个更重要的问题——什么时候该忍住不用继承。3.1 为什么继承值得学重复代码才是最大的敌人设想一个饮品系统。咖啡有名字、价格、豆子产地茶有名字、价格、茶叶种类果汁有名字、价格、是不是鲜榨。如果各写一个类__init__和describe会被复制三遍。复制本身不致命致命的是某天老板说「价格要改成含税显示」你得改三个地方漏一个就出线上事故。继承的做法是把三者的共同点抽到一个父类里子类只写自己独有的那部分。改价格逻辑只改父类一处三个子类自动跟着变。这就是书里说的「继承提供了抽象」——你不需要知道底下有多少种饮品只要知道它们都是饮品就有名字和价格。但继承还有一个更隐蔽的价值多态。当你写for d in drinks: print(d.describe())时你根本不用判断这是咖啡还是茶每个对象自己知道该怎么描述自己。这种「调用同一个方法名行为各不相同」的能力是后面学设计模式的基础。话说回来继承也是最容易被滥用的东西。新手学会继承后会忍不住给所有东西建父子关系最后搞出一棵五层深的继承树改一个父类的方法下面七八个子类全崩。这本书在第三章专门讲了菱形继承问题多个父类有共同祖先也讲了super()的调用顺序目的不是让你用多重继承而是让你在遇到的时候知道发生了什么。3.2 动手写一条继承链看清 super() 到底做了什么在上一个例子的基础上改。新建drinks.py# drinks.py class Drink: 所有饮品的共同部分有名字有价格能自我描述 def __init__(self, name: str, price: float) - None: self.name name self.price price def describe(self) - str: return f{self.name}{self.price:.1f} 元 class Coffee(Drink): 咖啡在饮品的基础上多了豆子产地 def __init__(self, name: str, price: float, beans: str) - None: super().__init__(name, price) # 公共部分交回父类绝不抄一遍 self.beans beans # 子类只负责自己多出来的那部分 def describe(self) - str: base super().describe() # 复用父类的描述再往后接 return f{base}豆子{self.beans} class Tea(Drink): def __init__(self, name: str, price: float, leaf: str) - None: super().__init__(name, price) self.leaf leaf def describe(self) - str: return f{super().describe()}茶叶{self.leaf}super().__init__(name, price)这一行白话解释就是把名字和价格的赋值工作交还给父类去做我不重复写。super()不是「调用父类的构造函数」这么生硬它更像是「沿着继承链往上找下一个该负责这块的家伙」。在单继承里它就是父类。describe方法里调用super().describe()是同一套思路父类已经会描述名字和价格了我拿过来在后面补一段就行不用重新拼一遍字符串。跑一下看看# demo2.py from drinks import Coffee, Tea items [Coffee(摩卡, 28.0, 埃塞俄比亚), Tea(龙井, 18.0, 明前)] for item in items: print(item.describe())输出两行格式统一但内容各不相同。这个循环里没有一句if isinstance(...)这就是多态最直观的样子。注意子类的__init__里如果忘了写super().__init__(...)父类那部分属性根本不会存在调用describe()时就会报AttributeError。这类错误不会在定义时报只有真正跑到那一行才炸排查起来很烦。写子类时先写 super 那行再写自己的。3.3 组合优先什么时候该把继承放下这里有一条业内流传很广的建议优先使用组合而不是继承。这句话不是教条它有非常具体的判断标准。判断方法很简单用「是一个」和「有一个」来测试。咖啡是一种饮品说得通用继承。但咖啡店有一个磨豆机不能说咖啡店是一种磨豆机这时候就该用组合——把磨豆机当成咖啡店的一个属性。# shop.py class Grinder: def grind(self, beans: str) - str: return f{beans} 磨好了 class CoffeeShop: 咖啡店有一个磨豆机而不是是一种磨豆机 def __init__(self, name: str) - None: self.name name self.grinder Grinder() # 组合把另一个对象装进来 def make(self, drink_name: str, beans: str) - str: step self.grinder.grind(beans) # 委托给它去干活 return f{self.name}{step}开始制作 {drink_name}组合的好处是松耦合。哪天你想把Grinder换成ElectricGrinder只要新类也有grind方法CoffeeShop一行都不用改。如果这里用的是继承换起来就得动继承链牵一发动全身。书里第五章「何时使用面向对象编程」讲的就是这个判断力什么时候该抽类、什么时候该用属性property、什么时候该引入一个管理器对象。这些不是靠背规则能学会的得先把手感练出来。入门阶段的建议是默认用组合只有当「是一个」说得通、并且你确实想复用父类的行为时才用继承。4. 新手最容易踩的坑五个让你卡到怀疑人生的地方这一章单独拎出来是因为下面这五个坑几乎每个人都会踩至少两个而且报错信息通常看不懂。书里第四章讲异常、第五章讲属性、第七章讲数据结构散落着这些问题的答案我把它们集中起来按「现象 → 原因 → 解决」写清楚遇到时直接对照。4.1 语法层面的三个坑第一个坑方法定义漏了self。现象调用obj.method()时报TypeError: method() takes 0 positional arguments but 1 was given你数了参数明明对得上。原因Python 在调用实例方法时会自动把实例本身作为第一个参数传进去。如果定义时没写self那个位置就空着方法收到的参数比预期多一个。解决把def describe():改成def describe(self):。如果方法确实不需要访问任何属性也可以加staticmethod装饰器但新手阶段先老老实实写self别急着用装饰器绕过去。第二个坑可变默认参数被所有实例共享。现象创建了两个购物车对象往第一个里加商品第二个里也冒出来了。原因看下面这段代码items[]这个默认值在函数定义的时候只创建一次之后所有不传参的调用共用同一个列表。# 错误写法 class Cart: def __init__(self, items[]): # 这个列表是全班共用的 self.items items # 正确写法 class Cart: def __init__(self, itemsNone): self.items list(items) if items else [] # 每个实例都拿一份新的解决默认值一律用None在函数体内部再创建真正的可变对象。这个坑不止出现在__init__里任何函数都可能中招是个通用陷阱。第三个坑把类属性当成实例属性用。现象给一个对象改了值所有对象的这个值都跟着变了。原因属性写在class下面、方法外面它就是类属性被所有实例共享。只有写在带self.的赋值语句里才是实例自己的。class Counter: total 0 # 类属性所有实例共享这一份 def __init__(self): self.count 0 # 实例属性每个对象各有一份 a, b Counter(), Counter() a.count 1 print(a.count, b.count) # 1 0互不影响 Counter.total 1 print(Counter.total) # 1类属性要认准类名去改解决想清楚这个数据是「每个对象一份」还是「大家共用一份」。状态类的数据一律放实例属性常量、配置项才考虑类属性。判断不清的时候默认放实例属性出问题的概率小得多。4.2 设计层面的两个坑第四个坑继承链套得太深。现象改了一个父类的方法跑起来一堆子类报错或者super()调到的方法和你以为的不是同一个。原因继承层数一多方法解析顺序MRO就变得难以预测尤其是出现多重继承的时候。书里第三章讲菱形继承讲的就是两条继承路径汇聚到同一个祖先时的麻烦。解决继承链控制在两层以内。需要复用第三层的东西时先问一句「这里能不能用组合」。真要写多重继承用print(SomeClass.__mro__)把顺序打出来看清楚别靠脑补。新手阶段能用单继承加组合解决的一律别碰多重继承。第五个坑在__init__里干了太多活。现象只想创建一个对象结果它连数据库、读配置、发请求全做了单元测试根本没法跑。原因很多人把__init__当成「启动流程」来用把所有初始化动作都塞进去。__init__的正确职责只有一件事把属性设好让对象处于一个合法的初始状态。解决__init__里只做赋值和轻量校验。需要连接资源、加载文件的单独写一个load()或connect()方法让调用方显式触发。这样测试的时候可以直接构造一个空壳对象不用连带把外部依赖都准备好。书里第十三章讲测试时反复强调的「可测性」源头就在这里。提示看到AttributeError: Xxx object has no attribute yyy先别怀疑人生顺着两条线查——一是__init__里有没有给yyy赋值二是子类有没有调用super().__init__()。九成的这类错误都在这两处。5. 让代码能扛事异常、测试和数据结构的入门三件套前面四章解决了「怎么把代码组织起来」这一章解决「怎么让它别一碰就碎」。书里从第四章开始讲异常、第七章讲数据结构、第十三章讲测试这三块内容看着分散其实是同一件事的三个面让你的程序在面对意外时还有话说。5.1 异常处理为什么你的程序一碰错就全崩新手写的脚本通常没有异常处理一旦输入不对整个程序从头到尾崩掉还甩出一大段红色堆栈。这在本地跑跑无所谓但任何要给被人用的东西都不能这样。异常处理的思路是把「可能出问题的操作」和「出问题之后怎么办」分开写。注意这不是让你把所有代码包进try里——那是另一种翻车方式错误被吞掉问题藏得更深。# orders.py class PriceError(ValueError): 价格不合理时抛出。继承 ValueError表示它属于值不对这一类 def make_order(name: str, price: float) - str: if not name.strip(): raise PriceError(商品名不能为空) if price 0: raise PriceError(f价格必须大于 0收到的是 {price}) return f已下单{name}{price:.1f} 元 # 调用方只处理自己认识的那一类错误 for name, price in [(摩卡, 28.0), (, 20.0), (拿铁, -5)]: try: print(make_order(name, price)) except PriceError as e: print(下单失败, e)跑起来会看到一行成功、两行失败但程序没有崩循环继续往下走。这就是异常处理最朴素的价值单个操作失败不影响整体流程。几个要点。raise是主动抛出异常别怕用它它比返回一个None或者-1让调用方去猜要清楚得多。自定义异常继承ValueError而不是继承Exception是因为这样调用方可以用except ValueError一把兜住所有「值不对」的错误粒度更合理。except后面一定要写具体的异常类型光写except:会把键盘中断、内存错误也一起吞掉调试时你会想砸键盘。书里第四章还有一句话值得记住异常并不「异常」。输入不合法、文件不存在、网络断了这些在真实世界里天天发生属于正常流程的一部分。把它们当成正常分支来设计代码反而更清楚。5.2 写第一个 pytest 测试三行代码换来的安全感测试这件事新手最容易觉得「等我写完再说」然后永远不说。其实第一个测试的成本低到你没有理由跳过。新建test_orders.py和orders.py放一起# test_orders.py import pytest from orders import make_order, PriceError def test_normal_order(): result make_order(摩卡, 28.0) assert 摩卡 in result def test_empty_name_raises(): # pytest.raises 用来断言这里必须抛错抛了才算通过 with pytest.raises(PriceError): make_order(, 20.0) def test_negative_price_raises(): with pytest.raises(PriceError): make_order(拿铁, -5)在终端执行pip install pytest然后pytest -q。看到绿色的三个点或者一句3 passed你的第一套测试就跑通了。test_开头的函数会被自动发现不需要注册。assert就是普通的断言失败时会打印出具体的值比unittest那套assertEqual直观得多这也是书里同时介绍两个框架后更推荐 pytest 的原因。pytest.raises是个上下文管理器用来验证「这里应该报错」。写测试的顺序建议反过来先写一个会失败的测试再写代码让它通过。这就是书里第十三章讲的测试驱动开发。刚开始会觉得别扭坚持两周你会发现自己改代码时心里有底了——改完跑一遍全绿就敢提交。这种安全感是任何我觉得应该没问题都给不了的。5.3 数据结构选错后面全是返工书里第七章专门讲 Python 的数据结构包括元组、命名元组、dataclass、字典、列表、集合和三种队列。新手阶段不需要全记住只要建立一个意识选错容器后面的代码会越来越别扭。最实用的一个建议是当你发现自己在写一个只有属性的类没有任何方法优先考虑dataclass。from dataclasses import dataclass dataclass(frozenTrue) class Sample: 一条样本记录。frozenTrue 表示创建后不可修改可以安全地当字典的键 length: float width: float label: str s Sample(5.1, 3.5, setosa) print(s) # Sample(length5.1, width3.5, labelsetosa) print(s.label) # setosadataclass会自动帮你生成__init__、__repr__和相等比较省掉十几行样板代码。frozenTrue让对象不可变好处是不可变对象可以当字典的键、可以放进集合也不会被别处的代码偷偷改掉。至于容器怎么选给一条粗糙但好用的判断需要按下标顺序访问用list需要按名字查找用dict需要去重或者判断「在不在里面」用set需要固定不变的一组值用tuple。书里还讲了defaultdict和Counter这两个省事的小工具等你写统计类代码时会感谢它们。6. 下一步学什么一条不迷路的进阶路线这本书十四章一口气读完不现实也没必要。我给一条我自己走过、也给不少人推荐过的顺序按阶段推进每阶段都有明确的「学会了」的判断标准。阶段大概投入重点内容判断学会的标准第一段一到两周类、实例、属性、方法、类型注解第 2 章能不看书写出一个带__init__和两个方法的类第二段两到三周继承、super()、组合优于继承第 3、5 章能说清一个场景该用继承还是组合并写出代码第三段两到三周异常、自定义异常、pytest 基础第 4、13 章能给自己的小项目写出五个以上的测试并跑绿第四段三到四周dataclass、字典与集合的选用、迭代器与生成器第 7、10 章能把一段for循环改写成生成器表达式第五段按需设计模式第 11、12 章、并发第 14 章在自己的代码里认出「这里其实是个策略模式」几个具体建议。第一段千万别跳很多人急着看设计模式结果连self都没搞明白看什么都是天书。第三段的测试要尽早插进来它是后面所有内容的地基没有测试你改任何东西都是蒙着眼睛走路。第四段的迭代器和生成器是这本书里性价比最高的部分之一。书里第十章用一个最近邻分类器的案例把迭代器协议、列表推导、生成器函数串起来讲你跟着敲一遍会发现以前写的很多for循环都可以压缩成一行而且内存占用还更小。至于设计模式我的建议是别当成课程学当成词典查。写代码时觉得「这里怎么这么别扭」再去翻第 11、12 章看看装饰器、观察者、策略、命令这几个是不是能对上号。硬背模式名字没有任何意义认出来的那一刻才有价值。最后一章讲并发线程、多进程、asyncio三套方案都摆出来了还对比了各自的坑。这部分可以等你有实际需求再看因为并发的问题往往不是「不会写」而是「不知道该选哪个」而选择能力来自踩过坑。我自己带新人的时候有个习惯每学完一段就让对方把之前写的一个小脚本用新学的知识重构一遍。第一次重构通常会把代码改得更长更复杂这很正常别慌。第三次之后你会开始删代码那才是真的学会了。从那以后我每次教人面向对象都强制对方走一遍「重构旧代码」这一步比做十个练习题都管用。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑