-面向对象编程 Lecture 12 - Object Oriented Pro
Python面向对象编程:继承与生成器
看完你能讲清继承、MRO、元类与描述符的边界与原理
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
Python面向对象编程:继承与生成器
看完你能讲清继承、MRO、元类与描述符的边界与原理
什么是继承
你长得像父母——眼睛、性格、甚至说话方式,这种"子代从亲代获得特征"的现象叫遗传。代码世界也有类似的"遗传",让新类不用从零造轮子,直接复用父类的能力。
DNA 对应父类定义,外貌对应属性,变异对应方法重写
继承的类层次结构
箭头 A→B 表示 A 是 B 的父类。任一子类沿箭头反向回溯,即得到它的祖先链。
为什么需要继承
你已经能画出一棵类继承树了——Student 在 Person 下面,VipStudent 又在 Student 下面。但为什么要把类组织成树形?继承正是为了解决三类问题。
上级定的规章下级自动遵守;总部改一次,全员生效
继承 vs 组合
写代码时最纠结的设计选择:让 Dog 继承 Animal,还是在 Dog 里塞个 Animal?选错会改一处崩一片。
- 语义:子类「是」父类的一种
- 实现:class 子类(父类)
- 耦合:强耦合,父类改动波及全盘
- 语义:主体「有一个」组成部分
- 实现:self.部件 = 其他类()
- 耦合:弱耦合,可运行时替换
类的定义与继承语法
用 Animal→Dog/Cat 完整例子,把继承语法、super 调用和方法重写一次看清。
L9 演示 `class 子类(父类)` 的核心写法;L12 用 super() 复用父类构造;L15 是同名方法自动重写;L26 用 isinstance 验证继承关系成立。
子类构造方法
上一页我们定义了 Dog 继承 Animal,但运行时报错——属性不存在。问题出在哪?子类也有'出生'流程,得先把父类那套初始化走一遍。
父亲先把房本立好,孩子才能在自己名下加房间
方法重写与方法扩展
子类继承了父类,但有时父类的实现不完全合适——可以彻底换掉它,也可以在它基础上加东西。两种策略对应不同的取舍。
覆盖是彻底换菜,扩展是先沿用总部招牌菜再加本地新菜
多重继承与MRO
上页讲单继承,子类只继承一个父类。但如果一个类想同时拥有多个父类的能力呢?Python 允许多继承,但带来一个新问题——菱形继承。
按顺序拨,接听者处理或上交链上下一个
成绩簿需求分析
前面学了继承语法、方法重写、MRO——但为什么要搞这么复杂?这页用真实的成绩簿,看需求怎么一步步把简单的 dict 逼成继承体系。
业主卡啥都能开、租户卡限本楼、访客卡限时段
基类:Gradebook
基类 Gradebook 统一封装添加、查询、统计三种能力,子类可直接继承复用。
基类不关心数据怎么存,只定义通用接口:add 录入、query 查询、average/stats 做统计。子类可以沿用或覆盖——这就是继承的复用价值。
子类:UGGradebook
本科生成绩簿继承 Gradebook:用 super() 复用父类录入,新增年级边界与按年级统计。
L1 是继承声明;L10/L15 先校验年级再用 super() 复用父类逻辑;L18 不动父类就扩展出新能力。
子类:GradGradebook
在基类上做加法:新增导师字段,并把成绩收紧为 P/NP。
L3 声明继承 Gradebook;L7 super() 复用父类构造;L8 是新增的 advisor 字段;L11/L16 用 super().xxx() 实现『扩展而非替换』。
成绩簿类图
UML 三栏式:类名 / 属性 / 方法;箭头由子类指向基类表示继承。
什么是生成器
成绩簿如果存一万名学生,列表会一次性吃光内存;但很多时候我们只需要'逐个处理'。生成器就是一家'随用随造'的工厂——你要一个,它造一个,绝不多造。
你要一杯才做一杯;函数也是 yield 一个才往下走一步
迭代器协议与生成器
从for循环出发,看Python如何通过iter()和next()驱动迭代,再对比「手写类」与「生成器」两种实现路径。
生成器函数:yield语句
用一段最小代码, 对比 yield 与 return 在函数里的不同语义。
yield 暂停并冻结执行栈; return 终止函数, 其值附在 StopIteration.value 上送出。
生成器表达式
对比列表推导式与生成器表达式,把『惰性』二字看明白。
方括号产 list、圆括号产 generator;type 一目了然。next() 一次拉一个,取过的不复存在——这就是『惰性』。
生成器的内存优势
前面看到生成器用yield一个一个产出值,但为什么不直接建个列表?答案在内存:面对海量甚至无限的元素,两种方式的差距是天壤之别。
拧开才出水、用多少流多少;列表则要提前灌满所有桶
生成器 vs 列表推导式
这里的生成器指生成器表达式;两者写法相似,却常因惰性求值、单次消费与结果复用而误选。
- 惰性求值:调用后不立刻算完
- 结果不落地,额外空间近 O(1)
- 只能单次消费,完整遍历通常较慢
- 立即求值:先算出全部结果
- 结果全部存储,空间为 O(n)
- 结果可重复使用,完整遍历通常更快
生成器的状态与限制
一个倒计时生成器跑两次 for 循环,第二次彻底沉默——生成器的单向性。
L4 yield 暂停并返回值;L7 仅创建对象不执行;L16 第二次 for 循环零输出——单向性铁证。
生成器链式处理
演示 yield from 把多个生成器串成一条数据管道
yield from 等价于 'for x in upstream: yield x',并把 .send/.throw/.return 委托给上游,所以管道能跨段正确传值与中断。
自测:继承与生成器
`yield from super().stream()` 逐项转交父类生成器;若父类生成器产生 `base`,再 `yield 'child'`,结果是哪组?
核心要点总结
- ✓继承表达 IS-A 关系,代码复用通过共享属性与方法实现
- ✓多重继承的菱形冲突由 C3 MRO 确定唯一查找顺序
- ✓能用组合就别用继承——组合只暴露接口,继承会泄露父类实现
- ✓yield 在暂停点保存执行栈帧,实现按需取值的惰性序列
- ✓生成器耗尽后不可重置,send()可向yield传值
课后思考
先独立思考再看参考答案——三题分别考验理解、应用与边界判断。
参考答案完全替换说明子类语义已变(Liskov 替换不再成立);用 super() 扩展说明子类是父类的特化,保留父类核心行为再叠加增量。
参考答案三个 yield 函数串联——读行、过滤级别、格式化输出——每步只持一行内存;列表必须先全量加载,10GB 直接爆掉。
参考答案C3 线性化按继承列表从左到右与各父类祖先顺序合并,保证每类只出现一次;super() 沿 MRO 链继续,菱形继承时每个类恰好被调用一次。