-面向对象编程 Lecture 12 - Object Oriented Pro

官方信息技术老师·25 页·深入(追求细节与边界)·0 次浏览·2 天前
OOPMRO元类描述符

Python面向对象编程:继承与生成器

看完你能讲清继承、MRO、元类与描述符的边界与原理

按 空格/→ 演示下一步

1 / 25 页

全部页面点击任意一页,跳回舞台从这页播放

OOPMRO元类描述符

Python面向对象编程:继承与生成器

看完你能讲清继承、MRO、元类与描述符的边界与原理

1第 1 页 · Python面向对象编程:继承与生成器

什么是继承

你长得像父母——眼睛、性格、甚至说话方式,这种"子代从亲代获得特征"的现象叫遗传。代码世界也有类似的"遗传",让新类不用从零造轮子,直接复用父类的能力。

is-a 关系
子类是父类的一种特例,满足里氏替换原则
继承成员
子类自动获得父类属性和方法,无需重写
扩展能力
在继承基础上新增自己的属性和方法
方法重写
同名方法覆盖父类,实现差异化行为
子女继承父母对应 →子类继承父类

DNA 对应父类定义,外貌对应属性,变异对应方法重写

2第 2 页 · 什么是继承

继承的类层次结构

箭头 A→B 表示 A 是 B 的父类。任一子类沿箭头反向回溯,即得到它的祖先链。

图解渲染中…
objPython 一切类的最终根类,默认隐式继承a1自定义顶层父类,封装通用行为c1孙子类,传递继承所有上层属性方法
3第 3 页 · 继承的类层次结构

为什么需要继承

你已经能画出一棵类继承树了——Student 在 Person 下面,VipStudent 又在 Student 下面。但为什么要把类组织成树形?继承正是为了解决三类问题。

代码复用
共有属性和方法抽到父类,子类不必重写
层次建模
用 is-a 关系映射现实分类,注意是单向传递
多态基础
父类引用可指向子类对象,调用时自动分发
公司组织架构对应 →类继承层次

上级定的规章下级自动遵守;总部改一次,全员生效

4第 4 页 · 为什么需要继承

继承 vs 组合

写代码时最纠结的设计选择:让 Dog 继承 Animal,还是在 Dog 里塞个 Animal?选错会改一处崩一片。

继承 (is-a)
  • 语义:子类「是」父类的一种
  • 实现:class 子类(父类)
  • 耦合:强耦合,父类改动波及全盘
组合 (has-a)
  • 语义:主体「有一个」组成部分
  • 实现:self.部件 = 其他类()
  • 耦合:弱耦合,可运行时替换
层级关系明确(如 Dog 是 Animal)用继承;只需复用对方能力时(如 Car 有 Engine),优先组合。
5第 5 页 · 继承 vs 组合

类的定义与继承语法

python

用 Animal→Dog/Cat 完整例子,把继承语法、super 调用和方法重写一次看清。

代码高亮加载中…

L9 演示 `class 子类(父类)` 的核心写法;L12 用 super() 复用父类构造;L15 是同名方法自动重写;L26 用 isinstance 验证继承关系成立。

6第 6 页 · 类的定义与继承语法

子类构造方法

上一页我们定义了 Dog 继承 Animal,但运行时报错——属性不存在。问题出在哪?子类也有'出生'流程,得先把父类那套初始化走一遍。

super() 用法
拿到父类的引用,在子类里代替父类名调用方法
传入父类参数
super().__init__(name) 按父类签名透传所需参数
必须放首行
先让父类初始化完,子类才能安全加自己的属性
孩子继承家业对应 →子类继承构造

父亲先把房本立好,孩子才能在自己名下加房间

7第 7 页 · 子类构造方法

方法重写与方法扩展

子类继承了父类,但有时父类的实现不完全合适——可以彻底换掉它,也可以在它基础上加东西。两种策略对应不同的取舍。

完全覆盖
子类重新定义同名方法,父类版本被遮蔽,不再执行
super() 扩展
先调用父类原版,再在前后加新逻辑,原有行为不丢
选择依据
父类逻辑完全不适用选覆盖;要保留并增强选扩展
连锁分店改菜单对应 →子类改方法

覆盖是彻底换菜,扩展是先沿用总部招牌菜再加本地新菜

8第 8 页 · 方法重写与方法扩展

多重继承与MRO

上页讲单继承,子类只继承一个父类。但如果一个类想同时拥有多个父类的能力呢?Python 允许多继承,但带来一个新问题——菱形继承。

菱形继承
两个父类继承自同一祖先,子类再继承这两个父类
MRO
方法解析顺序,Python 查找方法的线性序列
C3 算法
生成 MRO 的线性化算法,保证不重复不遗漏
super 沿 MRO
super 不是调父类,而是沿 MRO 链的下一个
查看 MRO
类.__mro__ 属性或 类.mro() 方法查看顺序
紧急联系人列表对应 →MRO 链

按顺序拨,接听者处理或上交链上下一个

9第 9 页 · 多重继承与MRO

成绩簿需求分析

前面学了继承语法、方法重写、MRO——但为什么要搞这么复杂?这页用真实的成绩簿,看需求怎么一步步把简单的 dict 逼成继承体系。

V1 简单记录
只存姓名和分数,一个字典就够
V2 角色登场
管理员、老师、学生三类人要用
V3 权限分化
老师改本班、学生只能看自己
V4 数据共用
所有角色操作同一份成绩库
小区门禁卡对应 →分级用户系统

业主卡啥都能开、租户卡限本楼、访客卡限时段

10第 10 页 · 成绩簿需求分析

基类:Gradebook

python

基类 Gradebook 统一封装添加、查询、统计三种能力,子类可直接继承复用。

代码高亮加载中…

基类不关心数据怎么存,只定义通用接口:add 录入、query 查询、average/stats 做统计。子类可以沿用或覆盖——这就是继承的复用价值。

11第 11 页 · 基类:Gradebook

子类:UGGradebook

python

本科生成绩簿继承 Gradebook:用 super() 复用父类录入,新增年级边界与按年级统计。

代码高亮加载中…

L1 是继承声明;L10/L15 先校验年级再用 super() 复用父类逻辑;L18 不动父类就扩展出新能力。

12第 12 页 · 子类:UGGradebook

子类:GradGradebook

python

在基类上做加法:新增导师字段,并把成绩收紧为 P/NP。

代码高亮加载中…

L3 声明继承 Gradebook;L7 super() 复用父类构造;L8 是新增的 advisor 字段;L11/L16 用 super().xxx() 实现『扩展而非替换』。

13第 13 页 · 子类:GradGradebook

成绩簿类图

UML 三栏式:类名 / 属性 / 方法;箭头由子类指向基类表示继承。

图解渲染中…
a1基类:通用数据存储与查询接口a2本科生子类:重写 add 与 statsa3研究生子类:重写 add 与 lookupextendsUML 中是空心三角箭头,此处简化
14第 14 页 · 成绩簿类图

什么是生成器

成绩簿如果存一万名学生,列表会一次性吃光内存;但很多时候我们只需要'逐个处理'。生成器就是一家'随用随造'的工厂——你要一个,它造一个,绝不多造。

yield 暂停
函数跑到 yield 就暂停,交出当前值
懒计算
要一个值才算一个,不预先生成全部
状态保持
暂停时的局部变量下次调用还在
节省内存
不用一次性存下所有结果
可迭代
生成器对象可直接放进 for 循环
现做现卖的奶茶店对应 →生成器

你要一杯才做一杯;函数也是 yield 一个才往下走一步

15第 15 页 · 什么是生成器

迭代器协议与生成器

从for循环出发,看Python如何通过iter()和next()驱动迭代,再对比「手写类」与「生成器」两种实现路径。

图解渲染中…
Biter()把可迭代对象变成迭代器Dnext()取值,耗尽时抛StopIterationL含yield的函数就是生成器M生成器自动实现__iter__和__next__
16第 16 页 · 迭代器协议与生成器

生成器函数:yield语句

python

用一段最小代码, 对比 yield 与 return 在函数里的不同语义。

代码高亮加载中…

yield 暂停并冻结执行栈; return 终止函数, 其值附在 StopIteration.value 上送出。

17第 17 页 · 生成器函数:yield语句

生成器表达式

python

对比列表推导式与生成器表达式,把『惰性』二字看明白。

代码高亮加载中…

方括号产 list、圆括号产 generator;type 一目了然。next() 一次拉一个,取过的不复存在——这就是『惰性』。

18第 18 页 · 生成器表达式

生成器的内存优势

前面看到生成器用yield一个一个产出值,但为什么不直接建个列表?答案在内存:面对海量甚至无限的元素,两种方式的差距是天壤之别。

列表的内存代价
一次性装下全部元素,n个值就要O(n)空间
生成器的延迟策略
每次next()才算出下一个值,算完即丢中间状态
常数级内存
无论n是1千还是1亿,始终只保留当前帧
适用场景
大文件逐行读取、无限序列、流水线式数据处理
水龙头对应 →生成器

拧开才出水、用多少流多少;列表则要提前灌满所有桶

Mlist(n)=O(n)vsMgen(n)=O(1)M_{\text{list}}(n) = O(n) \quad \text{vs} \quad M_{\text{gen}}(n) = O(1)
19第 19 页 · 生成器的内存优势

生成器 vs 列表推导式

这里的生成器指生成器表达式;两者写法相似,却常因惰性求值、单次消费与结果复用而误选。

生成器
  • 惰性求值:调用后不立刻算完
  • 结果不落地,额外空间近 O(1)
  • 只能单次消费,完整遍历通常较慢
列表推导式
  • 立即求值:先算出全部结果
  • 结果全部存储,空间为 O(n)
  • 结果可重复使用,完整遍历通常更快
当结果需要索引、切片或重复遍历时选列表推导式;当只单遍处理、数据很大或可由管道消费时选生成器。
20第 20 页 · 生成器 vs 列表推导式

生成器的状态与限制

python

一个倒计时生成器跑两次 for 循环,第二次彻底沉默——生成器的单向性。

代码高亮加载中…

L4 yield 暂停并返回值;L7 仅创建对象不执行;L16 第二次 for 循环零输出——单向性铁证。

21第 21 页 · 生成器的状态与限制

生成器链式处理

python

演示 yield from 把多个生成器串成一条数据管道

代码高亮加载中…

yield from 等价于 'for x in upstream: yield x',并把 .send/.throw/.return 委托给上游,所以管道能跨段正确传值与中断。

22第 22 页 · 生成器链式处理

自测:继承与生成器

点击作答

`yield from super().stream()` 逐项转交父类生成器;若父类生成器产生 `base`,再 `yield 'child'`,结果是哪组?

23第 23 页 · 自测:继承与生成器

核心要点总结

  • 继承表达 IS-A 关系,代码复用通过共享属性与方法实现
  • 多重继承的菱形冲突由 C3 MRO 确定唯一查找顺序
  • 能用组合就别用继承——组合只暴露接口,继承会泄露父类实现
  • yield 在暂停点保存执行栈帧,实现按需取值的惰性序列
  • 生成器耗尽后不可重置,send()可向yield传值
延伸主题:抽象基类与鸭子类型的取舍协程与async/await的演进描述符协议与属性控制
24第 24 页 · 核心要点总结

课后思考

先独立思考再看参考答案——三题分别考验理解、应用与边界判断。

1重写父类方法时,何时该用 super() 扩展、何时该完全替换?这背后反映了什么设计原则?

参考答案完全替换说明子类语义已变(Liskov 替换不再成立);用 super() 扩展说明子类是父类的特化,保留父类核心行为再叠加增量。

2如果要流式处理一个 10GB 的日志文件并提取所有 ERROR 级别行,你会怎么用生成器链?换成列表能做到吗?

参考答案三个 yield 函数串联——读行、过滤级别、格式化输出——每步只持一行内存;列表必须先全量加载,10GB 直接爆掉。

3多重继承的两个父类有同名方法时,Python 的 MRO 怎么决定调用顺序?方法内部用 super() 继续调用又会怎样?

参考答案C3 线性化按继承列表从左到右与各父类祖先顺序合并,保证每类只出现一次;super() 沿 MRO 链继续,菱形继承时每个类恰好被调用一次。

25第 25 页 · 课后思考