Python 如何理解继承、多态和鸭子类型?
简化版
继承用于复用和扩展父类能力,多态表示同一接口在不同对象上有不同行为。Python 更强调鸭子类型:不强制关心对象属于哪个类,只要它提供需要的方法,就可以被当作目标对象使用。
详细版
继承示例:
class Animal:
def speak(self):
raise NotImplementedError
class Dog(Animal):
def speak(self):
return "wang"
class Cat(Animal):
def speak(self):
return "miao"
多态:
def make_sound(animal):
print(animal.speak())
make_sound(Dog())
make_sound(Cat())
make_sound 不需要知道具体是狗还是猫,只需要调用 speak。
鸭子类型更进一步:
class Robot:
def speak(self):
return "beep"
make_sound(Robot())
即使 Robot 没有继承 Animal,只要它有 speak 方法,也能工作。
面试回答时要强调:Python 的多态不一定依赖继承层级,它更看重对象行为。继承适合表达稳定的 is-a 关系;如果只是组合能力,很多时候组合比继承更清晰。
完整版教学
一、继承解决什么问题
继承让子类复用父类代码,并在父类基础上扩展或改写行为。
class BaseView:
def render(self):
return "base html"
class UserView(BaseView):
def get_user(self):
return "Tom"
UserView 自动拥有 BaseView 的 render 方法。
继承适合表达“子类是父类的一种”:
Dog是一种Animal;Circle是一种Shape;AdminUser是一种User。
如果关系不是 is-a,就要谨慎使用继承。
二、多态是什么
多态的重点不是语法,而是“同一调用,不同行为”。
class Circle:
def area(self):
return 3.14
class Rectangle:
def area(self):
return 10
def print_area(shape):
print(shape.area())
传入 Circle,调用圆的 area;传入 Rectangle,调用矩形的 area。
调用方只面向共同能力写代码,不关心具体实现类型。
三、Python 的多态为什么很自然
在 Java 这类静态类型语言里,多态常依赖父类或接口:
Shape shape = new Circle()
Python 是动态语言,调用方法时才检查对象有没有这个方法。
def print_area(obj):
print(obj.area())
只要运行到这里时 obj 有 area 方法,就可以调用。没有的话才会报 AttributeError。
这让 Python 写多态代码时不一定需要显式接口。
四、鸭子类型怎么理解
鸭子类型的经典思想是:如果一个东西走起来像鸭子、叫起来像鸭子,那就可以把它当鸭子。
放到 Python:
def read_all(file_like):
return file_like.read()
read_all 不要求参数必须是某个具体文件类,只要对象有 read() 方法即可。
这让代码更灵活,尤其适合测试和适配不同实现。
五、鸭子类型的风险
灵活也带来风险。错误对象传进来时,问题可能在运行时才暴露。
read_all(123) # AttributeError: 'int' object has no attribute 'read'
为了降低风险,可以使用:
- 清晰的函数命名和文档;
- 类型注解;
- 抽象基类;
- 单元测试;
- 必要时做
hasattr或协议检查。
例如:
from typing import Protocol
class Readable(Protocol):
def read(self) -> str:
...
这能让静态检查工具理解“需要一个有 read 方法的对象”。
六、继承和组合如何取舍
继承很强,但层级过深会变难维护。
如果只是想复用一个能力,可以用组合:
class Logger:
def log(self, message):
print(message)
class Service:
def __init__(self, logger):
self.logger = logger
Service 不必继承 Logger,只需要持有一个 logger 对象。
常用判断:
- 符合 is-a,用继承;
- 符合 has-a,用组合;
- 只是临时需要某个行为,用鸭子类型。
七、常见误区与追问
记忆钩子:继承回答“它是谁的一种”,多态回答“同一个调用有不同实现”,鸭子类型回答“我不关心你是谁,只关心你会不会做这件事”。
- 误区:Python 的多态必须依赖继承。 Python 是动态语言,只要对象提供被调用的方法,就能参与同一段逻辑;继承只是组织多态的一种方式,不是唯一入口。
- 追问:鸭子类型和接口有什么区别? 鸭子类型偏运行时能力约定,接口或抽象基类偏显式契约;大型项目可以用
typing.Protocol或 ABC 把鸭子类型的边界写清楚。 - 误区:继承越多复用越强,设计越好。 深继承会让行为来源难追踪,父类改动影响范围大;仅为了复用工具能力时,组合通常比继承更稳。
- 追问:什么时候用抽象基类? 当你需要明确要求子类实现某些方法,并希望实例化前就暴露缺失实现时,可以用
abc.ABC和@abstractmethod。 - 误区:鸭子类型不需要测试。 鸭子类型依赖对象行为契约,缺少显式继承保护,更需要单元测试覆盖关键方法是否存在、返回值是否符合预期。
- 追问:组合为什么常被推荐? 组合把能力放到独立对象中,通过持有和委托复用行为,类之间耦合更低,也更容易替换实现。
八、加强记忆
继承关注类之间的 is-a 关系,多态关注同一接口的不同行为,鸭子类型关注对象有没有所需能力。Python 里不必为了多态强行设计继承树,只要对象能响应目标方法,代码就能工作;但大型项目要用类型注解、协议或抽象基类把边界讲清楚。