← 返回题目列表

Python 如何理解继承、多态和鸭子类型?

高频 中等 第 9 / 21 题 更新于 2026/07/27
继承多态鸭子类型面向对象

简化版

继承用于复用和扩展父类能力,多态表示同一接口在不同对象上有不同行为。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 自动拥有 BaseViewrender 方法。

继承适合表达“子类是父类的一种”:

  • 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())

只要运行到这里时 objarea 方法,就可以调用。没有的话才会报 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 里不必为了多态强行设计继承树,只要对象能响应目标方法,代码就能工作;但大型项目要用类型注解、协议或抽象基类把边界讲清楚。