
简介这份PPT课件面向零基础或刚接触编程的Python学习者聚焦面向对象编程这一进阶主题帮助读者从面向过程的线性思维过渡到以数据为核心的组织方式。课件共64页围绕类与实例、属性与方法、封装、继承、多态三大原则展开并用Dog类、动物分类层次等贴近现实的例子讲解抽象与实例化的关系同时回扣算法、数据结构、函数、模块等前置基础。资源包内含1个pptx文件约962KB图文排版完整、知识点衔接清晰适合课堂讲授、自学跟练或作为备课参考可配合前序课程按章节推进。目前已有282人学习下载读者可借助它理清类与对象的定义边界、方法调用的实现思路掌握继承层次与多态接口的设计逻辑为后续异常处理、模块化开发与项目实践打下面向对象的基础。1. 从 64 页 Python 面向对象课件说起类与对象到底解决什么问题很多人把 python基础语法 里的列表、字典、循环、函数都跑通了代码却还是越写越乱一个字典里塞了七八个键函数参数越传越长改一个字段要全局搜索确认没人漏改。第 14 讲那份 64 页的 Python 面向对象编程 课件正是冲着这道断层来的。它要回答的不是「类是什么」这种定义题而是当一堆数据和操作它们的行为纠缠在一起时怎么打包成一个整体让调用方少记参数、让修改只发生在一处。这份内容适合两类人。一类是刚装完 python在 vscode python环境配置 或 pycharm配置python环境 里跑通了第一个print的入门者需要从「写脚本」过渡到「写模块」另一类是有其他语言背景、却被 Python 的self、「没有 interface」、「类变量怎么会串味」搞晕的人。下面把课件里最容易被一页 PPT 带过的部分拆开self绑定的是谁、__init__何时执行、继承链上的属性查找顺序、以及为什么类属性会被所有实例共享。2. Python 类与对象的最小可用写法2.1 用 class 定义第一个类init与 self 的参数含义先看一段能直接跑的代码在终端或编辑器里新建student.py即可不需要额外依赖。# student.py class Student: school 示例中学 # 类属性所有实例共享一份 def __init__(self, name, score): # self 就是被创建出来的那个实例 self.name name # 实例属性每个对象各自一份 self.score score def is_pass(self, threshold60): # 方法第一个参数永远是 self调用时不用手动传 return self.score threshold s1 Student(小林, 78) s2 Student(小周, 52) print(s1.name, s1.is_pass()) # 小林 True print(s2.name, s2.is_pass()) # 小周 False print(s1.school, s2.school) # 示例中学 示例中学 s1.school 另一所学校 # 注意这不是修改类属性 print(s1.school, s2.school, Student.school) # 输出另一所学校 示例中学 示例中学逻辑说明__init__不是「构造函数」真正分配对象的是__new____init__只做初始化。self由解释器自动传入s1.is_pass()完全等价于Student.is_pass(s1)这就是定义方法时必须留出第一个位置参数的原因。最后三行的输出值得盯一眼给s1.school赋值并不会改到Student.school而是在s1自己的实例字典里新建了一个同名条目把它「盖住」了。参数说明threshold60是默认参数调用时可以不传。如果想让及格线全局可配可以写成类属性pass_line 60方法里读self.pass_line代价是这个规则被绑死在类上将来做多学校复用时不如显式传参灵活。提示self只是约定俗成的名字写成this也能正常执行但所有风格检查工具和代码阅读习惯都按self来别标新立异。2.2 从函数式写法迁移到对象写法一张对照表迁移前常见写法是「字典 一堆函数」# 迁移前函数 字典 def add_score(record, value): record[scores].append(value) def average(record): scores record[scores] return sum(scores) / len(scores) if scores else 0.0 stu {name: 小林, scores: []} add_score(stu, 78) add_score(stu, 91) print(round(average(stu), 2)) # 84.5迁移后数据和操作数据的行为被收进同一个类# 迁移后类 class ScoreBook: def __init__(self, name): self.name name self.scores [] def add(self, value): if isinstance(value, bool) or not isinstance(value, (int, float)): raise TypeError(分数必须是数字) if not 0 value 100: raise ValueError(分数应在 0 到 100 之间) self.scores.append(value) return self # 返回 self支持链式调用 def average(self): return sum(self.scores) / len(self.scores) if self.scores else 0.0 book ScoreBook(小林).add(78).add(91) print(book.name, round(book.average(), 2), book.scores) # 小林 84.5 [78, 91]两种写法的差别用表格看更直观维度纯函数写法面向对象写法数据存放字典 / 元组分散传递收敛到实例属性行为归属散落在模块各处类内方法参数个数每个函数都要重复传self自带上下文扩展新字段改所有相关函数签名主要改__init__状态校验靠每个调用方自觉可在入口统一拦截逻辑说明校验逻辑从「每个调用点都要写一遍」变成「写一次就拦在门口」。add返回self是常见手法让多个操作串成一行但链式调用在出错时堆栈会变长业务分支复杂时反而降低可读性不建议到处用。参数说明isinstance(value, (int, float))第二个参数传元组表示「满足其中任一类型即可」。注意bool是int的子类True会被判定为数字所以前面单独排除了isinstance(value, bool)否则add(True)会悄悄塞进 1 分。2.3 实例属性、类属性与类型转换的常见组合先记住三种属性写法各自的归属和影响范围写法归属读取顺序修改影响self.x 1实例实例字典优先只影响当前对象Cls.x 1类实例找不到才回退到类影响所有未覆盖的实例self.__x实例名字被改写同上只影响当前对象查看一个对象身上到底挂了什么最直接的办法是vars(obj)或obj.__dict__调试时比断点还快。下面这段是从配置字符串构造对象的常见场景几乎每个做 python教程 练习的人都会踩一次类型转换的坑import json class Config: def __init__(self, raw): data json.loads(raw) if isinstance(raw, str) else raw self.host str(data.get(host, 127.0.0.1)) self.port int(data.get(port, 8000)) # 9000 会被转成 9000 self.debug self._to_bool(data.get(debug, False)) staticmethod def _to_bool(value): # bool(False) 的结果是 True字符串必须单独处理 if isinstance(value, str): return value.strip().lower() in (1, true, yes, on) return bool(value) def __repr__(self): return fConfig(host{self.host!r}, port{self.port}, debug{self.debug}) print(Config({host: 0.0.0.0, port: 9000, debug: False})) # Config(host0.0.0.0, port9000, debugFalse)逻辑说明int()能安全处理数字字符串但遇到9000.0会抛ValueError生产代码里通常要包一层 try 或改用float()再取整。bool()只认空字符串、0、None为假字符串False是非空字符串所以恒为True——这是 python类型转换 里最经典的一处陷阱。参数说明_to_bool用staticmethod声明它不依赖实例状态也不需要self。反序列化时给每个字段设默认值比事后 try/except 补字段要省事得多默认值本身也充当了一份「字段清单」。3. 封装、继承与多态三大机制在 Python 里怎么落地3.1 单下划线与双下划线Python 的「私有」到底有多私Python 没有真正的private。单下划线是团队约定「外部别碰」双下划线会触发名字改写name mangling在类体外访问时需要用改写后的名字class Wallet: def __init__(self, balance): self._balance balance # 约定仅内部使用 self.__pin 1234 # 被改写为 _Wallet__pin def pay(self, amount): if amount 0: raise ValueError(金额必须为正) if amount self._balance: raise ValueError(余额不足) self._balance - amount return self._balance w Wallet(100) print(w.pay(30)) # 70 print(w._balance) # 70语法上能访问但违背约定 print(w.__dict__) # {_balance: 70, _Wallet__pin: 1234}逻辑说明名字改写的目的是防止子类无意中覆盖父类的内部属性而不是安全防护。w._Wallet__pin依然能读到所以真正的敏感信息不要只靠下划线来保护。子类里定义同名的__pin不会被改写冲突因为改写的名字里带了各自的类名。参数说明pay里两个检查顺序会影响报错信息——先判断金额是否为正再判断余额能避免pay(-5)报出「余额充足」这种误导性结果。金额如果是浮点数累减后可能出现99.69999999999999涉及钱款的场景建议用decimal.Decimal或者统一按「分」存整数。3.2 继承与 super()什么时候该用组合代替继承继承要解决的是「is-a」关系而组合解决「has-a」。一个储蓄账户是账户所以继承合理class Account: def __init__(self, owner, balance0): self.owner owner self.balance balance def deposit(self, amount): self.balance amount return self.balance class SavingsAccount(Account): def __init__(self, owner, balance0, rate0.03): super().__init__(owner, balance) # 公共字段交给父类初始化 self.rate rate def add_interest(self): self.balance * (1 self.rate) return round(self.balance, 2) s SavingsAccount(小林, 1000, 0.03) print(s.deposit(500), s.add_interest(), isinstance(s, Account)) # 1500 1545.0 True逻辑说明super()不是「调用父类」而是「沿 MRO方法解析顺序找下一个类」。查看顺序用SavingsAccount.__mro__输出里会依次列出SavingsAccount、Account、object。多继承场景下这个区别很关键super()会在 MRO 链上继续往后走而不是跳回「亲爹」。参数说明子类__init__里如果忘了调super().__init__()父类的属性根本不会创建调用deposit时就会抛AttributeError。反过来如果一个类只是「用了」另一个类的功能比如订单需要记录日志就不要为了复用几行代码去继承 Logger直接把 logger 作为参数传进来组合更合适。3.3 多态与鸭子类型不写 interface 也能统一调用只要对象有对应方法就能被同一段代码使用——这就是鸭子类型。三种约束方式对比如下方式需要共同基类检查时机典型用途鸭子类型否调用时日志输出、序列化、插件抽象基类 ABC是实例化时框架规定的插件接口typing.Protocol否静态检查器类型提示 解耦from typing import Protocol class Sink(Protocol): def write(self, text: str) - None: ... class FileSink: def write(self, text): with open(out.txt, a, encodingutf-8) as f: f.write(text \n) class BufferSink: def __init__(self): self.items [] def write(self, text): self.items.append(text) def emit(sink: Sink, lines): for line in lines: sink.write(line) buf BufferSink() emit(buf, [alpha, beta]) print(buf.items) # [alpha, beta]逻辑说明Protocol定义了一份「形状」FileSink和BufferSink都不用显式继承它静态检查器在传参时按形状匹配即可。运行时不做强制校验跑错对象要到方法调用那一刻才报AttributeError所以关键入口处补一句hasattr检查或isinstance判断是常见做法。参数说明emit的sink参数只依赖write一个方法这是接口最小化的好处——将来接入网络上报、内存队列、控制台打印都不用改emit一行代码。用Protocol时记得它不是运行时基类别拿它去做isinstance判断除非加runtime_checkable但那种判断只检查方法名不检查签名。4. 面向对象编程实例把图书借阅需求写成可运行的类4.1 需求拆解三个类各自负责什么一份 python面向对象编程实例 想讲清楚最忌讳把所有逻辑堆进一个大类。按职责拆成三块最稳类职责关键属性关键方法Book单本书的库存与借出记录isbn、total、_borrowedborrow、give_back、overdueReader读者的借阅上限与在借清单name、limit、holdingcan_borrowLibrary汇总操作入口books字典add、lend、take_back拆分的判断标准是谁的数据谁负责改。让Library.lend去直接改Book._borrowed短期少写两个方法长期就会出现两处代码操作同一份状态。4.2 完整代码图书借阅最小系统from datetime import date, timedelta class Book: def __init__(self, isbn, title, total1): self.isbn isbn self.title title self.total total self._borrowed {} # {读者名: 到期日} property def available(self): return self.total - len(self._borrowed) def borrow(self, reader_name, days14): if self.available 0: raise RuntimeError(f《{self.title}》已全部借出) self._borrowed[reader_name] date.today() timedelta(daysdays) def give_back(self, reader_name): self._borrowed.pop(reader_name, None) # 不存在也不报错 def overdue(self, todayNone): today today or date.today() return [r for r, due in self._borrowed.items() if due today] class Reader: def __init__(self, name, limit3): self.name name self.limit limit self.holding [] def can_borrow(self): return len(self.holding) self.limit class Library: def __init__(self): self.books {} # {isbn: Book} def add(self, book): self.books[book.isbn] book return self def lend(self, isbn, reader, days14): book self.books.get(isbn) if book is None: raise KeyError(f查无此书{isbn}) if not reader.can_borrow(): raise RuntimeError(f{reader.name} 已达上限 {reader.limit}) book.borrow(reader.name, days) reader.holding.append(isbn) return self def take_back(self, isbn, reader): self.books[isbn].give_back(reader.name) reader.holding.remove(isbn) return self lib Library().add(Book(978-7, Python 入门, total1)) lin Reader(小林) lib.lend(978-7, lin) print(lin.holding, lib.books[978-7].available) # [978-7] 0 try: lib.lend(978-7, Reader(小周)) except RuntimeError as e: print(拦截, e) # 拦截《Python 入门》已全部借出 lib.take_back(978-7, lin) print(lib.books[978-7].available) # 1逻辑说明property把available变成只读计算属性调用时写book.available而不是book.available()外部无法给它赋值避免了「库存数」和「借出记录」两个数据源不一致。_borrowed用字典而非列表是为了在give_back时按读者名直接定位省去遍历。参数说明give_back用pop(key, None)而不是pop(key)还书操作因此具备幂等性重复归还不会抛异常。lend里用books.get(isbn)加显式判断比self.books[isbn]直接取更友好能抛出自定义的「查无此书」而不是一个光秃秃的KeyError。days14在Book.borrow和Library.lend两处都出现实际项目里应只保留一处把它提到配置里。4.3 运行验证与常见报错对照跑之前先确认解释器版本python --version输出 3.8 以上都行Protocol需要 3.8。常见报错和对应原因整理如下报错常见原因排查动作AttributeError: Book object has no attribute _borrowed子类或改写版本忘了调super().__init__()检查__init__首行TypeError: int object is not callable把 property 当方法调了又或用了同名内置名去掉括号或给变量改名KeyError: 978-7借书前没先add或 isbn 拼写不一致打印lib.books.keys()ValueError: too many values to unpackfor里解包字典只写了键名改成.items()或单变量接收验证时建议把每一步的中间状态打出来比如借出前后各打一次available归还后再打一次。比起一次性运行完整流程再看结果逐步打印更容易定位是状态没变还是变了但读错了属性。5. 进阶与排错slots、类变量陷阱与调试手法当类的字段固定且实例数量很大时给类加__slots__可以省掉每个实例的__dict__内存占用明显下降属性访问也略快class Point: __slots__ (x, y) # 只允许这两个属性 def __init__(self, x, y): self.x x self.y y p Point(1, 2) p.z 3 # AttributeError: Point object has no attribute z代价是失去了动态加属性的能力也和多重继承、__dict__反射、functools.cached_property有冲突。数据量大且结构固定的场景比如解析百万行记录值得用普通业务类加上只会给自己添麻烦。比__slots__更常见也更隐蔽的是可变类变量陷阱class Cart: items [] # 错误示范所有实例共享同一个列表 def add(self, name): self.items.append(name) a, b Cart(), Cart() a.add(apple) print(b.items) # [apple] —— b 也被污染了正确的写法是在__init__里初始化class Cart: def __init__(self): self.items [] # 每个实例一份规则很简单类属性只放不可变常量或共享的配置可变容器一律放进__init__。用dataclasses时也有同样的问题可变默认值必须写成field(default_factorylist)不能直接写 []。调试阶段值得顺手记几个手段vars(obj)看实例字典、type(obj).__mro__看继承链、dir(obj)列出全部可访问名字、obj.__class__.__name__在日志里打类型名。要查某个属性到底来自实例还是类直接x in vars(obj)判断即可比反复读代码快得多。如果模块里要同时处理多个相互独立的对象集合且每个对象的初始化都涉及阻塞 I/O考虑用concurrent.futures或multiprocessing把实例构造分发出去这时要注意对象不一定可跨进程序列化带锁、文件句柄、数据库连接的实例传不过去通常的做法是在子进程里各自重建对象只传构造参数。本文还有配套的精品资源点击获取