📄 一句话概括
企鹅是鸟,但鸟都会飞不成立;最糟的是继承下来之后不报错,只是逻辑错了。
📝 本节和前面几节的关系
2.5 说比喻会歪,这一节给出最典型的例子——这是本章标题的正题。
一、企鹅这个反例
class Penguin(Bird),Bird 里有 fly。
实测 isinstance(p, Bird) 是 True——现实里企鹅确实是鸟。
实测 p.fly() 返回 【飞起来了】——错得很彻底,但程序不报错。
「企鹅是鸟」成立,「鸟都会飞」不成立。两句话常被当成一句话用。
二、为什么它不报错
因为继承只检查类型关系,不检查能力是否成立。
Penguin 是 Bird 的子类,所以它【有】fly 这个方法——至于这个方法对它有没有意义,编译器不管。
最糟的就是这种错:不报错,只是逻辑是错的。它会在某个业务场景里悄悄出问题。
三、分类与能力是两回事
分类(鸟/哺乳动物)回答【它是什么】。
能力(会飞/会游泳)回答【它能做什么】。
现实里这两样常常一致,但软件里必须分开表达:
会飞的实现一个 fly 接口,不会飞的不实现。分类管分类,能力管能力。
四、更麻烦的一类
员工是人,成立;人都会休假,不成立(合同工、外包、实习生规则都不同)。 凡是【都会…】的句子,都要停下来验一遍。 验法很简单:找一个反例。找得到,就不能写进父类。
五、动手 Demo:面向对象和现实世界是似是而非的
让企鹅继承鸟,然后调一次 fly。
看它返回什么、会不会报错、以及 isinstance 怎么说。
class Bird:
def fly(self): return "飞起来了"
class Penguin(Bird):
pass
p = Penguin()
print("是鸟吗 ", isinstance(p, Bird))
try:
print("fly() ", p.fly())
print("报错吗 没有")
except Exception as e:
print("报错 ", type(e).__name__)
实跑输出:
--------------------------------------------------------------------
面向对象和现实世界是似是而非的
--------------------------------------------------------------------
【似是而非】四个字,最经典的例子是**企鹅**。
class Penguin(Bird): pass
p = Penguin()
表达式 实测结果 对照现实
--------------------------------------------------------
isinstance(p, Bird) True 现实里:企鹅是鸟
p.fly() 飞起来了 现实里:企鹅不会飞
报错了吗 没有 这才是最糟的
**最糟的就是第三行:不报错,只是逻辑是错的。**
现实里的分类(鸟)和软件里的能力(会飞)**不是一回事。**
现实里的说法 软件里成立吗 为什么
-------------------------------------------------------
企鹅是鸟 成立 分类关系对
鸟都会飞 不成立 能力不能靠分类继承
员工是人 成立 分类关系对
人都会休假 看情况 合同工和正式工不一样
**正解是把【能力】单独拎出来**:会飞的一类实现 `fly`,不会飞的不实现。
**分类管分类,能力管能力,别用一根继承线串起来。**
实测:是鸟=True,fly() 照样返回【飞起来了】,不报错。
最糟的就是这种:不报错,只是逻辑是错的。
30 秒实验
- 找一个你写过的【都会…】的父类方法,看看有没有反例。
- 把企鹅的例子改成接口,看调用方会多写什么、换来什么。
常见误区
- 以为现实里的分类能直接当继承用。分类管是什么,能力管能做什么。
- 以为不报错就是对的。逻辑错比语法错更难查。
自测三问(点开对答案)
1. 企鹅这个例子说明什么? —— 【企鹅是鸟】成立,【鸟都会飞】不成立;继承只检查类型关系,不检查能力是否成立。 2. 为什么这种错最难查? —— 因为它不报错,程序照常跑,只是逻辑错了,会在某个业务场景里悄悄出问题。 3. 怎么处理分类和能力的冲突? —— 把能力单独拎出来做接口:会飞的实现 fly,不会飞的不实现;分类管分类,能力管能力。- ☐ 下一节(2.7 明确定义为编程结构)
相关笔记
- 继承对共同点和不同点进行系统的分类和整理 — 继承的正题
- 明确定义为编程结构 — 拆掉比喻后的正式定义
- ☕ Java基础 · 索引 — 接口与抽象类
- 🧩 面向对象是怎样工作的 · 索引