首页 / 资讯中心 / 文章详情

Python面向对象编程入门:从类与对象到继承多态和异常处理

Python面向对象编程入门:从类与对象到继承多态和异常处理 ★ FEATURED ARTICLE
简介《Python Object-Oriented ProgrammingFourth Edition》是Packt于2021年出版、由Steven F. Lott与Dusty Phillips合著的英文PDF电子书面向希望系统掌握Python面向对象编程并构建健壮、可维护应用与库的初中级开发者。内容围绕封装、继承、多态等核心机制展开结合类与实例、属性与方法等Python实现方式延伸到模块化设计、异常处理、单元测试、文档编写以及版本控制、依赖管理和持续集成等工程实践。资源包仅含1个PDF文件大小约6.51MB便于离线阅读与检索目前已有43人学习。读者可借此梳理OOP知识框架理解如何把设计原则落到可维护的Python代码与库维护流程中适合作为案头参考和进阶阅读材料。1. 先别被“面向对象”四个字唬住它其实是给代码分工的一种办法刚学 Python 的人大多经历过这个阶段几十行脚本跑得好好的一旦写到两三百行改一个变量名就有三处跟着报错函数参数越加越长最后连自己都记不清哪个参数是干什么的。Python 面向对象编程要解决的就是这件事——把数据和对数据的操作捆成一个个“对象”每个对象只负责自己那部分出了问题也容易定位。它不是给熟手准备的玄学恰恰是写给被一团乱麻的脚本折磨过的新手。这门技术适合的读者很明确能用 Python 写出循环、函数、列表和字典但一写大一点的程序就组织不起来的人。从写第一个类开始把零散的代码变成能维护的小项目就是这本书要走完的路。2. 搞懂对象和类第一个例子从“员工工牌”开始写起2.1 为什么光会写函数早晚要碰类先用函数式写法实现一个小需求根据名字和工号打印工牌。很多人会写成两个列表加一个函数names [小明, 小红] ids [1001, 1002] def make_badge(names, ids, index): # 通过下标同时取两个列表里同一位置的数据 return f{ids[index]} - {names[index]}这样写当然能跑但问题会随着需求增长慢慢浮现。第一两个列表必须严格同步删掉一个人时忘了删另一个列表里的工号所有人都会错位。第二一旦要加第三个字段比如部门就得改函数的参数列表所有调用处都要跟着改。第三这段代码没有告诉别人“名字和工号是一组数据”读代码的人只能靠猜。我第一次写这种代码时在一个员工管理脚本里堆了五个平行列表跑了一周才发现某次删除操作只删了其中三个列表数据对不上的也不报错。这就是典型的“数据和操作分家”带来的问题。类的作用就是把一组相关的数据和对它们的操作放进同一个容器里用一个变量就能带走全部。2.2 第一个类十几行代码就能跑起来下面这个类只做一件事保存一个人的名字和工号并能打印一张工牌。class Employee: 员工工牌只保存名字和工号并提供打印格式。 def __init__(self, name: str, emp_id: int) - None: # self 指向“正在被创建的这个对象”本身 self.name name # 每个实例各自保存一份名字 self.emp_id emp_id # 每个实例各自保存一份工号 def badge(self) - str: # 把对象自己的两条数据拼成一行文字返回 return f{self.emp_id} - {self.name}逐行拆开看。class Employee:是定义一个类类名习惯上用大驼峰。__init__是初始化方法创建对象时自动执行用来把初始数据装进去它的第一个参数self是 Python 自动传入的代表当前这个对象新手不用手动传。self.name name的意思是“把外面传进来的 name 存到这个对象自己身上”。badge是一个方法本质上就是绑定在类里的函数它读的是self.name和self.emp_id所以每个对象打印出来的都是自己的信息。接着创建两个对象试试a Employee(小明, 1001) # 创建一个对象__init__ 自动被调用 b Employee(小红, 1002) # 再创建一个两者互不影响 print(a.badge()) # 输出1001 - 小明 print(b.badge()) # 输出1002 - 小红 a.name 小明同学 # 改 a 的名字 print(b.name) # 仍然是“小红”b 不受影响参数说明name是字符串emp_id是整数badge()不接受额外参数返回一个字符串。逻辑上Employee是“模板”a和b是按模板造出来的两个独立实体这种实体在术语里叫“实例”。模板只有一份实例可以有很多份各自的数据互不干扰——这正是平行列表做不到的事。要注意一个细节类里定义的方法第一个参数必须写self调用时却不用传a.badge()和Employee.badge(a)是等价的。新手最常见的报错TypeError: badge() takes 0 positional arguments but 1 was given基本就是忘了写self。2.3 类型提示与文档字符串给三个月后的自己留张字条上面的代码里name: str、emp_id: int、- str这些写法叫类型提示。它们不会在运行时强制检查写错了也不会当场报错但好处很实在编辑器会提前标红明显的错误同事和你自己半年后回来看一眼就知道每个参数该传什么。这本书从第二章就开始大量使用类型提示原因也在这里——面向对象代码里参数多、层次深没有标注会非常难读。类型提示可以和默认值配合给参数留一个“可以不传”的余地from typing import Optional class Employee: def __init__(self, name: str, emp_id: int, dept: Optional[str] None) - None: self.name name self.emp_id emp_id self.dept dept # 不传部门时就是 None def badge(self) - str: if self.dept is None: return f{self.emp_id} - {self.name} return f{self.emp_id} - {self.name} [{self.dept}]Optional[str]表示“要么是字符串要么是 None” None是默认值。这里有一个需要特别注意的问题默认值不要用列表、字典这类可变对象否则所有实例会共享同一个对象前面有人往里加数据后面创建的对象就会莫名其妙多出内容。需要可变默认值时写dept: Optional[list] None再在方法里判断并新建。除了类型提示员工工牌……这种三引号字符串叫文档字符串写在类或函数的第一行。用help(Employee)就能把它打印出来。它不影响运行但能让别人不读源码也知道这个类干什么。入门阶段建议每个类都写一句哪怕只有十几个字。2.4 模块和包别把所有类都塞进一个文件当类多起来之后全部塞进一个.py文件会变得很难找。Python 的解决办法很朴素一个.py文件就是一个模块一个带__init__.py的文件夹就是一个包。按职责拆开找起来就快。project/ ├── models/ │ ├── __init__.py │ └── employee.py └── main.py# models/employee.py class Employee: def __init__(self, name: str, emp_id: int) - None: self.name name self.emp_id emp_id# models/__init__.py # 把这个包对外的入口集中在这里外面只需 from models import Employee from .employee import Employee# main.py from models import Employee # 绝对导入从项目根目录开始写全路径 e Employee(小明, 1001) print(e.badge() if hasattr(e, badge) else e.name)参数与逻辑说明models是包名employee是模块名from .employee import Employee里的点表示“当前这个包内部”这叫相对导入适合包内部互相引用from models import Employee是绝对导入从项目顶层开始写可读性更好。入门阶段建议优先用绝对导入因为相对导入在脚本直接被运行时容易报ImportError: attempted relative import with no known parent package这是一个很多人第一次都会掉进去的坑。拆模块的判断标准不需要太复杂一个文件超过三四百行、或者明显能分成“数据定义”和“流程控制”两类内容时就该拆了。拆的时候注意避免循环导入——A 模块导入 BB 又导入 APython 会直接报错。真遇到了通常说明两个类的职责纠缠在一起需要重新划分而不是靠改导入语句硬绕。3. 继承与多态为什么“复制一份改改”迟早会翻车3.1 继承回答的是“是不是一种”的问题写代码写久了总会遇到两个类有大量重复字段的情况。比如员工和经理都有名字和工号经理还多一个“带队人数”。最直接的做法是把Employee整个复制一份改个名字加个字段。短期能跑长期是灾难以后改工牌格式两处都得改漏一处就出现两种不一致的工牌。继承的思路是把公共部分放在父类子类声明“我是一种父类”自动获得父类的字段和方法只写自己独有的部分。class Employee: def __init__(self, name: str, emp_id: int) - None: self.name name self.emp_id emp_id def badge(self) - str: return f{self.emp_id} - {self.name} class Manager(Employee): # 括号里写父类名表示继承 def __init__(self, name: str, emp_id: int, team_size: int) - None: super().__init__(name, emp_id) # 先让父类把公共字段装好 self.team_size team_size # 再补子类独有的字段 def badge(self) - str: base super().badge() # 复用父类的拼接逻辑 return f{base} (带队 {self.team_size} 人)参数说明Manager的__init__多了team_size前两个参数原样交给super().__init__。super()的意思是“找到父类调用它的同名方法”这样父类以后改了字段初始化逻辑子类会自动跟上。badge里的super().badge()同理先拿父类结果再加工避免把格式字符串抄两遍。判断该不该用继承有个很实用的自问句“子类是不是父类的一种”经理是员工的一种成立汽车有四个轮子轮胎不是汽车的一种那就该用组合——把轮胎作为汽车的一个属性而不是让汽车继承轮胎。这个判断标准简单粗暴但能挡掉大部分误用。3.2 多态一个函数吃下所有子类继承带来的最大好处叫多态。看下面这个函数def print_badges(people: list[Employee]) - None: for person in people: print(person.badge()) # 不关心具体是员工还是经理 print_badges([ Employee(小明, 1001), Manager(小红, 1002, 5), # 经理也能直接传进来 ])输出会分别是普通格式和带“带队 5 人”的格式。关键在于print_badges里只写了person.badge()它不需要知道对方到底是哪个类只要对象有这个方法就调用。以后再加一个“外包人员”类只要也继承Employee并实现badge这个函数一行都不用改。这就是多态的实际价值新增类型时改动集中在新类本身而不是散落在所有调用处。反过来如果一开始用if 类型 经理这种判断来区分每加一种类型就要去翻遍所有 if漏掉一处就是线上问题。3.3 多重继承和菱形问题知道有这回事就行Python 允许一个类同时继承多个父类叫多重继承。它能用但新手阶段建议先别碰。原因是一个经典难题如果 A 是基类B 和 C 都继承 AD 又同时继承 B 和 C那么 D 调用某个方法时到底走 B 那条线还是 C 那条线这个结构画出来像一个菱形所以叫菱形问题。Python 用一套叫 MRO方法解析顺序的规则来决定大致是按声明顺序从左到右、并且保证子类先于父类。规则本身是确定的但人脑跟踪起来很费劲尤其是继承层次深的时候。给新手的建议很直接能用组合解决就别用多重继承。组合的意思是“把另一个对象当作自己的属性”比如Manager里放一个list存下属而不是让Manager去继承list。这样职责清晰也不会踩到 MRO 那些需要反复推理的情况。真需要多继承时往往是在用“混入”类这种模式那是后面阶段的事。4. 异常与数据校验让程序出错时别整个崩掉4.1 为什么脚本总是死在“最后一行格式不对”处理文件的脚本有个共同命运前面九千行都跑得好好的最后一行多了个空行或者写了个“无”整个程序直接抛异常退出前面算好的结果全丢。这不是运气问题而是没有为“数据不合法”这种情况做准备。看一个典型写法def parse_scores(path: str) - list[int]: scores [] with open(path, encodingutf-8) as f: for line in f: scores.append(int(line.strip())) # 遇到空行或文字就炸 return scoresint()会抛ValueErrorint(无)也会。程序在这里中断后面的行一行都没处理。面向对象写法里异常不是“出事了”而是一种正常的沟通方式函数发现自己处理不了就明确抛出来让调用方决定怎么办。4.2 try / except 的最小可用写法改一下上面的函数让它遇到坏数据时跳过并记录而不是整体崩掉def parse_scores(path: str) - list[int]: scores [] with open(path, encodingutf-8) as f: for line_no, line in enumerate(f, start1): # 记下行号方便定位 text line.strip() if not text: # 空行直接跳过不算错误 continue try: scores.append(int(text)) except ValueError as e: # 只捕获“转数字失败”这一种 print(f第 {line_no} 行不是数字已跳过{text}{e}) return scores参数说明path是文件路径返回一个整数列表enumerate(f, start1)让行号从 1 开始方便报错时定位。逻辑上try里只放可能出问题的那一句except里只处理这一种异常。有两条需要特别注意的地方。第一不要写裸的except:那会把键盘中断、内存不足这些也一起吞掉程序看起来在跑其实已经不正常了。第二except里至少要留点痕迹打印一行或者记日志否则数据悄悄少了你都不知道排查起来毫无线索像面对一个黑匣子。4.3 自定义异常给错误起一个能看懂的名字内置异常类型有限业务里的问题往往需要更明确的表达。比如分数必须在 0 到 100 之间超范围时希望调用方一眼看懂是什么错class InvalidScoreError(ValueError): 分数超出合理范围时抛出继承 ValueError 便于统一捕获。 def __init__(self, value: int, low: int 0, high: int 100) - None: super().__init__(f分数 {value} 不在 {low}-{high} 范围内) self.value value # 把出问题的原始值带上方便上层处理 def check_score(value: int) - int: if not 0 value 100: raise InvalidScoreError(value) # 主动抛出而不是返回一个特殊值 return value参数说明value是待检查的分数low和high是允许范围默认 0 到 100。继承ValueError的好处是调用方既可以精确捕获InvalidScoreError也可以用except ValueError一把兜住灵活度高。使用时try: check_score(120) except InvalidScoreError as e: print(f数据有问题{e}原始值是 {e.value})自定义异常的名字本身就是文档。InvalidScoreError比一个-1返回值清楚得多也比打印一句“出错了”更容易在上层做分类处理。入门阶段不用建很多按业务里最常出问题的那两三种情况各建一个就够。5. 新手学面向对象最容易踩的 5 个坑5.1 概念上的三个坑坑一把所有东西都塞进一个类。现象写了一个System类里面有读文件、算数据、打印报表、发邮件十几个方法文件超过八百行改任何一处都怕碰坏别处。原因把“一个程序”当成了“一个对象”。类的划分依据是职责不是“都属于这个项目”。解决按“谁负责什么”拆。读文件归一个类计算归一个类输出归一个类。判断标准很简单描述这个类时如果要用“并且”连接两件不相关的事就该拆了。坑二一上来就用多重继承。现象刚学会继承觉得一个类继承三四个父类很高级结果方法调用走向完全猜不准改父类时子类行为莫名其妙变了。原因多重继承的解析顺序需要专门推理层次一深就超出人脑跟踪范围。解决先用单继承加组合。真要复用多个来源的能力优先把功能拆成独立对象用属性持有而不是用继承纳入。坑三属性随便改没有任何约束。现象对象的某个字段被赋了明显不合理的值比如年龄变成负数、状态变成不认识的字符串程序跑到很后面才出错排查时完全不知道是谁改的。原因Python 的属性默认完全开放赋值不做检查。解决在赋值入口做校验。简单场景可以在方法里判断后抛异常需要拦截obj.attr x这种写法时用property加 setter把校验逻辑集中在属性定义处。这样错误在发生的那一刻就暴露而不是等到几十行之后。5.2 写法上的两个坑坑四可变对象当默认参数。现象def __init__(self, tags: list [])结果第一个实例往tags里加东西第二个实例创建出来就已经带着那条数据了。原因默认参数在函数定义时只求值一次所有实例共享同一个列表对象。解决默认值写None在方法里判断if tags is None: tags []每次创建都得到一个新列表。这个坑和面向对象无关但在类里出现频率极高。坑五在__init__里做太多事。现象一创建对象就去读文件、连数据库、发网络请求测试的时候没法单独构造一个对象跑一次要等好几秒还经常因为环境问题失败。原因把“初始化数据”和“执行副作用”混在了一起。解决__init__只负责把参数存到属性上重量级操作放到单独的方法里由调用方在合适的时机触发。这样对象可以随时轻量创建测试时也能只测需要的那部分。6. 下一步学什么从“会写类”到“能写小项目”的路线学完前面这些你已经能定义类、写方法、用继承和多态也知道异常该怎么抛怎么接。接下来的问题不是“还学什么概念”而是“按什么顺序练才不至于学一堆零散知识却拼不出东西”。我给零基础读者的建议路线是这样按阶段推进每个阶段都配一个能跑通的小练习阶段目标建议练习第一周把类写顺用类重写你以前写过的任意一个脚本比如记账、待办清单第二周用好内置数据结构用dataclass定义数据类用字典和列表管理一组对象第三周会拆模块把练习拆成两三个.py文件用绝对导入串起来第四周会处理坏数据给练习加上异常处理和输入校验故意喂错误数据看反应第五周会写测试用pytest给核心方法写三五个用例跑通再改代码第六周认识常见模式读一读迭代器、策略、观察者这几种模式各写一个十行小例子几个具体提醒。第一dataclass值得早点用它能把一堆self.x x自动生成代码立刻短一截。第二测试不要等到“项目做大了再补”从第五周开始每加一个方法就配一个用例习惯养成了后面省很多事。第三设计模式不要背看懂一个就在自己的练习里套一次套不上就说明场景不对换一个再试。关于阅读顺序我的建议是先把类、继承、异常这三块吃透再去看数据结构和迭代器最后碰测试和并发。并发那块对新手来说门槛明显更高线程共享内存、多进程开销、异步的事件循环任何一个都能让人卡住好几天等前面基础扎实了再回头啃效率会高很多。我自己的血泪经验是曾经为了赶进度跳过测试直接写一个大类结果改一个字段名要手动点开十几个地方确认最后花的时间比从头写还多。从那以后我每次新建一个类都强制先把最小的那个方法写出来、跑通、再往上加绝不一口气写完两百行再运行。慢就是快这句话在面向对象里特别成立。这份资源把从类的基础到测试、并发的完整路径都铺好了适合当作长期翻的手边书而不是从头到尾读一遍就放下。挑一章对着敲一遍比读三章更有用。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站