📄 本篇摘要
多态(polymorphism)=同一个方法名,不同类各自实现——调用方只管喊 speak(),猫喵狗汪各答各的。接口=一份方法名清单(契约),谁签了谁必须实现。多态是【三大特性】的集大成者:让调用方与具体类型解耦。
一、多态:同一声令下,各自行动
for animal in [Dog("旺财"), Cat("咪咪"), Robot("R2")]:
print(animal.speak()) # 同一行代码,三种结果
调用方完全不知道(也不需要知道)列表里装的是什么——只知道它们都会 speak()。加一个新动物 Duck,这一行循环一个字都不用改。这就是多态的威力:新增类型不改旧代码(开闭原则的日常形态)。
二、接口:一份契约
接口(interface)只声明【有哪些方法】,不写实现:
- Java:
interface Speakable { void speak(); }——类必须implements并实现全部方法,编译器强制; - Python:鸭子类型是事实接口——【走起来像鸭子叫起来像鸭子,那它就是鸭子】:不看血统(类型),只看行为(有没有那个方法)。此外
abc模块可以显式定义抽象基类强制实现; - C++:抽象类(纯虚函数)扮演接口角色(C++ 11.5 讲过)。
接口的意义:把【能做什么】与【怎么做】分离——调用方依赖接口(能做什么),实现方各自发挥(怎么做)。
三、为什么多态是三大特性的集大成
- 封装(5-18)藏起实现细节;
- 继承(5-16)建立类型家族;
- 多态在家族之上统一调用——依赖接口而非具体类,变化被隔离在家族内部。
没有多态的代码长什么样?if isinstance(x, Dog): ... elif isinstance(x, Cat): ...——每加一个类型就要翻遍所有 if 链。多态把【类型判断】从调用方赶走了。
四、动手 Demo:鸭子类型与抽象接口两面夹击
from abc import ABC, abstractmethod
class Shape(ABC): # 显式接口:契约
@abstractmethod
def area(self): ...
class Rect(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r ** 2
class Triangle: # 不继承 Shape —— 鸭子类型照样入队
def __init__(self, base, height):
self.base, self.height = base, height
def area(self): # 有 area 就算数
return self.base * self.height / 2
shapes = [Rect(3, 4), Circle(2), Triangle(6, 5)]
print([f"{s.area():.2f}" for s in shapes])
print("总面积:", f"{sum(s.area() for s in shapes):.2f}")
try:
Shape() # 抽象类不许实例化
except TypeError as e:
print("拦截:", str(e)[:38], "...")
class Bad(Shape):
pass
try:
Bad() # 签了契约不实现也拦
except TypeError as e:
print("拦截:", str(e)[:38], "...")
沙箱实跑输出:
['12.00', '12.57', '15.00']
总面积: 39.57
拦截: Can't instantiate abstract class Shape ...
拦截: Can't instantiate abstract class Bad with ...
看到什么算成功:三种图形各自算面积、总面积一次求和;抽象类与【不实现 area 的子类】都被拦。
读结果:求总面积的循环不关心图形是什么——只关心有没有 area()。Triangle 连 Shape 都没继承也能混进来(鸭子类型);而 Bad 签了契约不履约,abc 在实例化时拦下。Python 两条路都通:松的靠自觉,紧的靠 abc。
五、30 秒实验
给 Triangle 改名方法为 get_area(),重跑总面积——TypeError:float 对象不可调用(None 没有 area)。鸭子类型的契约虽然不成文,违背了一样当场爆炸——只是爆炸时机从【定义时】推到了【运行时】。
六、常见误区
| 误区 | 真相 |
|---|---|
| 多态=方法重载 | 重载(同名不同参,编译期分);多态(同名同参,运行期按对象分)是两回事 |
| Python 没有接口 | 有鸭子类型这个事实接口;要硬契约用 abc 模块 |
| 多态让代码更绕 | 反了:它把类型 if 链全部赶走;新增类型零改动才是简化 |
自测三问(点击展开)
1. **多态给调用方带来什么?** 答:只依赖【有某方法】这一事实,不用知道具体类型;新增类型不改旧代码。 2. **鸭子类型是什么?** 答:不看类型血统,只看有没有相应方法——行为即类型。 3. **接口的意义?** 答:把能做什么(契约)与怎么做(实现)分离,双方各自演化。- ☐ 5-19 学习完成
相关笔记
- C++ 11.4 · 多态 —— 虚函数与动态绑定的深水区
- C++ 11.5 · 抽象类 —— C++ 的接口角色
- 继承与多重继承 —— 家族是前提
- ⌨️ 编程原理 · 索引 —— 全课路线图