29·模块七:面向对象编程进阶

多态与鸭子类型

ooppolymorphismduck-typing

Python 编程:从零基础到精通

第 29 课 - 多态与鸭子类型

模块七:面向对象编程 | 难度:Intermediate


1. 学习目标

通过本课学习,你将能够:

  • 理解多态的核心概念,并解释其在面向对象编程中的作用。
  • 使用继承实现多态,通过方法重写让不同子类对同一消息作出不同响应。
  • 掌握 Python 中的鸭子类型,理解"如果它走起来像鸭子,叫起来也像鸭子,那它就是鸭子"的编程哲学。
  • 比较继承多态与鸭子类型多态的适用场景,并能灵活运用。
  • 编写更灵活、可扩展的代码,减少对具体类型的依赖。

2. 核心概念

多态 (Polymorphism)

多态 是面向对象编程的三大特性之一(另外两个是封装和继承)。它的字面意思是"多种形态"。在编程中,多态指的是同一个接口(或方法名)可以作用于不同类型的对象,并且每个对象都能以自己的方式响应这个接口调用

想象一下"打印"这个动作:

  • 对于一个文本文件,打印意味着输出文字到打印机。
  • 对于一张图片,打印意味着输出图像到打印机。
  • 对于一个 PDF 文档,打印意味着输出排版好的页面。

虽然都是"打印",但针对不同的对象类型,具体行为不同。这就是多态。

鸭子类型 (Duck Typing)

鸭子类型是 Python 实现多态的一种更灵活、更动态的方式。它的核心思想是:不关心对象是什么类型,只关心它有没有需要的方法或属性

这个术语来自一句谚语:"如果它走起来像鸭子,叫起来也像鸭子,那它就是鸭子。" 在编程中,这意味着只要一个对象拥有正确的方法(比如 walk()quack()),我们就可以把它当作鸭子来使用,而不需要它真正继承自 Duck 类。

关键区别

  • 继承多态:依赖于类的继承体系和 is-a 关系(如"圆形是形状的一种")。
  • 鸭子类型:依赖于对象拥有的方法和属性,与继承无关(是 has-acan-do 关系)。

3. 代码示例

示例一:通过继承实现多态

from abc import ABC, abstractmethod

# 定义一个抽象基类 (Abstract Base Class)
class Shape(ABC):
    @abstractmethod
    def area(self):
        """计算面积"""
        pass

    @abstractmethod
    def perimeter(self):
        """计算周长"""
        pass

# 子类 1:圆形
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

    def perimeter(self):
        return 2 * 3.14159 * self.radius

# 子类 2:矩形
class Rectangle(Shape):
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * (self.length + self.width)

# 多态的使用:一个函数可以处理所有形状
def print_shape_info(shape):
    # 这里 shape 可以是任何实现了 area() 和 perimeter() 的对象
    print(f"形状类型: {type(shape).__name__}")
    print(f"面积: {shape.area():.2f}")
    print(f"周长: {shape.perimeter():.2f}")
    print("---")

# 创建对象
circle = Circle(5)
rectangle = Rectangle(4, 6)

# 同一函数,不同行为 - 这就是多态
print_shape_info(circle)
print_shape_info(rectangle)

示例二:鸭子类型 - 不依赖继承

# 这个类没有继承任何特定的基类
class Bird:
    def speak(self):
        return "叽叽喳喳!"

    def fly(self):
        return "鸟儿展翅飞翔!"

# 另一个完全无关的类
class Airplane:
    def speak(self):
        return "轰隆隆!"

    def fly(self):
        return "飞机引擎轰鸣,翱翔蓝天!"

# 这个类甚至没有 speak 方法
class Fish:
    def swim(self):
        return "鱼儿在水中游泳!"

# 鸭子类型演示:只要"像鸟一样能飞能叫",就能当"鸟"用
def bird_behavior(creature):
    # 不检查类型,只检查是否有方法
    if hasattr(creature, 'speak') and hasattr(creature, 'fly'):
        print(f"生物说: {creature.speak()}")
        print(f"飞行: {creature.fly()}")
    else:
        print(f"{type(creature).__name__} 不能像鸟一样行为。")

# 测试
print("=== 测试小鸟 ===")
bird = Bird()
bird_behavior(bird)

print("\n=== 测试飞机 ===")
plane = Airplane()
bird_behavior(plane)

print("\n=== 测试鱼 ===")
fish = Fish()
bird_behavior(fish)

示例三:实际应用 - 多态的游戏角色系统

import random

class Character:
    """游戏角色基类"""
    def __init__(self, name, health):
        self.name = name
        self.health = health
        self.alive = True

    def attack(self):
        """攻击方法,子类必须重写"""
        raise NotImplementedError

    def take_damage(self, damage):
        self.health -= damage
        if self.health <= 0:
            self.health = 0
            self.alive = False
            print(f"{self.name} 已被击败!")

class Warrior(Character):
    def attack(self):
        damage = random.randint(10, 20)
        print(f"{self.name} (战士) 挥剑攻击,造成 {damage} 点伤害!")
        return damage

class Mage(Character):
    def attack(self):
        damage = random.randint(5, 30)
        print(f"{self.name} (法师) 释放魔法弹,造成 {damage} 点伤害!")
        return damage

class Archer(Character):
    def attack(self):
        damage = random.randint(15, 25)
        print(f"{self.name} (弓箭手) 射箭,造成 {damage} 点伤害!")
        return damage

# 战斗函数 - 利用多态
def battle(char1, char2):
    round_num = 1
    print(f"\n⚔️ 战斗开始: {char1.name} VS {char2.name} ⚔️")

    while char1.alive and char2.alive:
        print(f"\n--- 第 {round_num} 回合 ---")
        
        # 无论 char1 是战士、法师还是弓箭手,都调用 attack()
        damage1 = char1.attack()
        char2.take_damage(damage1)
        
        if not char2.alive:
            break
            
        damage2 = char2.attack()
        char1.take_damage(damage1)
        
        round_num += 1

    winner = char1 if char1.alive else char2
    print(f"\n🏆 获胜者是: {winner.name}!")

# 创建不同类型的角色
warrior = Warrior("阿尔法", 100)
mage = Mage("梅林", 80)
archer = Archer("罗宾汉", 90)

# 任意两个角色都能战斗
battle(warrior, mage)
# battle(mage, archer)  # 可以再测试其他组合

4. 实践练习

练习 1:基础多态 (易)

创建一个 Vehicle 基类,包含 start_engine()stop_engine() 方法(可以用 print 输出信息)。然后创建 CarMotorcycleBicycle 三个子类。Bicycle 的引擎方法应该输出"自行车没有引擎"。编写一个函数 operate_vehicle(vehicle),依次调用这两个方法。

预期输出

汽车引擎启动!
汽车引擎关闭!

练习 2:鸭子类型计算器 (中)

设计一个简单的计算系统,包含 AdderMultiplier 两个类。每个类都有 calculate(a, b) 方法。编写一个函数 perform_calculation(calculator, x, y),它应该能接受任何有 calculate 方法的对象。

要求

  1. Adder.calculate 返回 a + b
  2. Multiplier.calculate 返回 a * b
  3. 函数不检查具体类型,只检查是否有 calculate 方法

预期输出

3 + 4 = 7
3 * 4 = 12

练习 3:插件系统 (难)

设计一个简单的"文件处理器"插件系统。有一个 process_file(filename, processor) 函数,processor 是一个对象。创建 TextProcessorImageProcessor 两个插件类,它们都有 process(content) 方法。TextProcessor 将文本转为大写,ImageProcessor 返回"处理图片数据"。

额外要求:加入错误处理,当传入的对象没有 process 方法时,给出友好提示。

预期输出

处理文本文件: HELLO WORLD!
处理图片文件: 处理图片数据
错误: 传入的对象没有 process 方法

5. 常见错误

错误 1:过度依赖 isinstance() 检查

# 不推荐的写法 - 违反了多态原则
def make_sound(animal):
    if isinstance(animal, Dog):
        animal.bark()
    elif isinstance(animal, Cat):
        animal.meow()
    # 每添加一个动物都要修改这个函数

# 推荐的写法 - 使用多态
def make_sound(animal):
    animal.make_sound()  # 调用相同的方法名

后果:代码难以维护,每次添加新类型都要修改现有函数。

错误 2:接口不一致

class Circle:
    def get_area(self):  # 方法名 get_area
        return 3.14 * self.radius ** 2

class Square:
    def area(self):  # 方法名 area
        return self.side ** 2

# 这个函数会失败,因为接口不统一
def total_area(shapes):
    return sum(shape.area() for shape in shapes)

后果:运行时错误,因为某些对象缺少相应方法。

错误 3:忽视鸭子类型的安全检查

# 不安全的鸭子类型使用
def process_data(processor, data):
    return processor.process(data)  # 如果没有 process 方法会崩溃

# 更安全的做法
def process_data(processor, data):
    if hasattr(processor, 'process'):
        return processor.process(data)
    else:
        raise TypeError("processor 需要实现 process 方法")

后果:程序在运行时因属性错误而崩溃。

错误 4:混淆多态和方法重载

# Python 没有传统的方法重载!
class Calculator:
    def add(self, a, b):
        return a + b
    
    # 这不是重载,这是重新定义,会覆盖第一个方法
    def add(self, a, b, c):
        return a + b + c

后果:只有最后一个定义生效,第一个 add 方法丢失。


6. 小结

关键要点回顾

  1. 多态是面向对象的核心特性,允许同一接口在不同对象上表现不同行为。

  2. Python 通过两种方式实现多态

    • 继承多态:基于类层次结构和方法重写。
    • 鸭子类型:基于对象拥有的方法和属性,与继承无关。
  3. 鸭子类型是 Python 的哲学:"不检查是什么,只检查能做什么"。

  4. 最佳实践

    • 优先考虑鸭子类型,使代码更灵活
    • 当需要强制接口时,使用抽象基类(ABC)
    • 保持方法名和接口一致
    • 适当使用 hasattr() 进行安全检查
  5. 多态的好处

    • 代码更灵活,易于扩展
    • 减少条件判断语句
    • 提高代码复用性
    • 支持"开闭原则"(对扩展开放,对修改关闭)

下一课预告:我们将学习 Python 的魔术方法(如 __str__, __repr__),这些特殊方法让你能够自定义类的行为,实现更高级的多态和操作符重载。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「魔术方法(如 __str__, __repr__)」 以巩固所学知识。