前言
多态(polymorphism)的字面意思是"多种形态":同一个调用、同一个方法名,作用在不同类型的对象上,会各自产生对应的行为。它解决的核心问题是把调用方和实现方解耦——调用方只说"给我算个面积",不必知道手里拿的到底是圆、矩形还是三角形。
关于多态,有两个特别常见的误解:
第一,以为在 Python 里必须先有继承、甚至必须先有抽象基类,才能实现多态。实际情况是,Python 的多态主要靠鸭子类型(duck typing):只要对象有同名的方法,调用就能成功,跟它继承自谁没有必然关系。继承只是组织代码的众多手段之一,不是多态的前提。
第二,以为"多态"就是写一长串if isinstance(x, A): ... elif isinstance(x, B): ...。这恰恰是反模式:真正的多态是把差异下沉到各个类自己的方法实现里,调用方只写一次x.area()。
本文先讲多态在 Python 里的落地方式,再讲它和isinstance的关系,最后给一个可以直接复制运行的例子。
一、多态的本质:调用方只认方法名
多态描述的是一个调用点:obj.area()。这套机制成立只需要一个条件——obj所属的类里能找到area这个方法。至于这个方法是怎么实现的、这个类继承自哪里、有没有实现某个接口,调用点一概不关心。
# 适用于 Python 3.8+
class Circle:
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r ** 2
class Rectangle:
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
def total_area(shapes):
return sum(s.area() for s in shapes)
shapes = [Circle(1), Rectangle(2, 3)]
print(total_area(shapes))Circle和Rectangle之间没有任何继承关系,total_area照样能工作。这就是鸭子类型:它管的是"你能不能响应area这个请求",而不是"你是谁的孩子"。Python 官方词汇表对这种风格的描述是——不看对象的类型,而是直接调用方法或使用属性,用接口而不是具体类型来获得灵活性。
二、继承式多态:把公共接口写进基类
鸭子类型灵活,但缺少约束。当一组类确实共享一整套接口时,用继承把公共方法名固定下来,可读性和可维护性更好,也更方便静态类型检查器工作。
# 适用于 Python 3.8+
class Shape:
def area(self):
raise NotImplementedError("子类必须实现 area()")
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r ** 2
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h基类Shape.area故意只抛NotImplementedError,这是"声明接口"的常见写法。但要注意,这种约束是软约束:它只在运行时调用到才会报错,而且如果某个子类压根不定义area,只要不去调它,程序也不会崩。想要编译期(准确说是导入/实例化时)就拦住,需要抽象基类abc.ABC加@abstractmethod,那是更强的一种约束。
三、多态与 isinstance:什么时候真需要类型检查
多态和isinstance并不冲突,它们是两个层面的工具。多态用于分发行为;isinstance用于做决策,比如选择要不要把对象交给某个函数。
isinstance(x, C)会检查x是不是C的实例,包括直接、间接和虚拟子类(通过register()注册的类)。它的第二个参数还可以是类型元组,只要是其中之一就返回True。
# 适用于 Python 3.8+
def describe(value):
if isinstance(value, (int, float)):
return f"数值:{value}"
if isinstance(value, str):
return f"文本长度:{len(value)}"
return "未知类型"这里用isinstance是合理的,因为函数确实要按类型分支。但不要用type(value) == int替代它:type()不看继承链,子类实例会被判为不相等,也会漏掉虚拟子类。
四、运算符多态与函数多态
多态不限于普通方法。Python 的运算符也是靠特殊方法(special method,也就是常说的 dunder 方法)实现多态的:+会去调__add__,len()会去调__len__,for会去调__iter__。
# 适用于 Python 3.8+
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
return Money(self.amount + other.amount)
def __repr__(self):
return f"Money({self.amount})"
print(Money(3) + Money(4)) # Money(7)注意__add__里返回NotImplemented(不是NotImplementedError):这是告诉解释器"我不认识这个类型,你去试试对方对象的__radd__",是运算符多态的协作约定。
常见坑点
1. 用type()判等代替isinstance()
❌if type(obj) == Shape:—— 子类的实例会被判为False。
✅if isinstance(obj, Shape):—— 会沿着继承链正确识别子类与虚拟子类。
2. 以为没有继承就没有多态
❌ 认为"必须写class Circle(Shape)才能让total_area工作"。
✅ 只要对象有area()方法即可,鸭子类型不要求继承关系。
3. 用一长串isinstance分支代替多态
❌if isinstance(s, Circle): ... elif isinstance(s, Rectangle): ...—— 每加一种形状都要改这个函数。
✅ 让每个类自己实现area(),调用方只写s.area()。
4. 基类抛NotImplementedError却指望一实例化就报错
❌Shape()能正常创建,直到调area()才炸。
✅ 需要更早拦截时改用abc.ABC+@abstractmethod,这类类根本不允许实例化。
5. 定义__eq__后对象突然不可哈希
❌ 只写了__eq__,结果对象放进set报TypeError: unhashable type。
✅ Python 3 中一旦类定义了__eq__而没有定义__hash__,__hash__会被置为None;需要可哈希时显式补上__hash__。
6.__add__里返回了NotImplementedError而不是NotImplemented
❌return NotImplementedError—— 这是把异常类当成值返回,运算结果会变成一个"异常类对象",完全错误。
✅return NotImplemented—— 这是值,告诉解释器转去尝试反向运算。
7. 鸭子类型下用hasattr判断时拼错方法名
❌if hasattr(obj, "arae"):—— 拼写错误永远返回False,还会掩盖真正的实现问题。
✅ 名字要与实现严格一致,或用try/except AttributeError(EAFP 风格)更稳妥。
8. 覆盖父类方法时把签名改了
❌ 父类def render(self, width),子类写成def render(self, width, height),多态调用时传一个参数直接TypeError。
✅ 覆盖方法保持与父类兼容的调用签名,多出来的能力用带默认值的参数补齐。
总结
| 多态的实现方式 | 关键机制 | 典型场景 |
|---|
| 鸭子类型 | 只要求对象有同名方法 | 松耦合的工具函数、脚本 |
| 继承 + 方法覆盖 | 子类重写父类方法 | 有明确层次关系的类型族 |
| 抽象基类 | abc.ABC+@abstractmethod | 需要强制实现接口 |
| 运算符多态 | 特殊方法如__add__、__len__ | 自定义数值/容器类型 |
| 参数多态 | 函数对入参类型不做限制 | 泛型风格的通用函数 |
多态的目的不是"用上某个语法",而是让调用方只依赖行为、不依赖具体类型。在 Python 里,鸭子类型是默认风格,继承和抽象基类是在需要约束时叠加的更强工具。判断一段代码是不是用了多态,看的是调用点是否只认方法名,而不是看它写没写class X(Y)。