Python 编程:从零基础到精通
第 29 课 - 多态与鸭子类型
模块七:面向对象编程 | 难度:Intermediate
1. 学习目标
通过本课学习,你将能够:
- 理解多态的核心概念,并解释其在面向对象编程中的作用。
- 使用继承实现多态,通过方法重写让不同子类对同一消息作出不同响应。
- 掌握 Python 中的鸭子类型,理解"如果它走起来像鸭子,叫起来也像鸭子,那它就是鸭子"的编程哲学。
- 比较继承多态与鸭子类型多态的适用场景,并能灵活运用。
- 编写更灵活、可扩展的代码,减少对具体类型的依赖。
2. 核心概念
多态 (Polymorphism)
多态 是面向对象编程的三大特性之一(另外两个是封装和继承)。它的字面意思是"多种形态"。在编程中,多态指的是同一个接口(或方法名)可以作用于不同类型的对象,并且每个对象都能以自己的方式响应这个接口调用。
想象一下"打印"这个动作:
- 对于一个文本文件,打印意味着输出文字到打印机。
- 对于一张图片,打印意味着输出图像到打印机。
- 对于一个 PDF 文档,打印意味着输出排版好的页面。
虽然都是"打印",但针对不同的对象类型,具体行为不同。这就是多态。
鸭子类型 (Duck Typing)
鸭子类型是 Python 实现多态的一种更灵活、更动态的方式。它的核心思想是:不关心对象是什么类型,只关心它有没有需要的方法或属性。
这个术语来自一句谚语:"如果它走起来像鸭子,叫起来也像鸭子,那它就是鸭子。" 在编程中,这意味着只要一个对象拥有正确的方法(比如 walk() 和 quack()),我们就可以把它当作鸭子来使用,而不需要它真正继承自 Duck 类。
关键区别:
- 继承多态:依赖于类的继承体系和
is-a关系(如"圆形是形状的一种")。 - 鸭子类型:依赖于对象拥有的方法和属性,与继承无关(是
has-a或can-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 输出信息)。然后创建 Car、Motorcycle 和 Bicycle 三个子类。Bicycle 的引擎方法应该输出"自行车没有引擎"。编写一个函数 operate_vehicle(vehicle),依次调用这两个方法。
预期输出:
汽车引擎启动!
汽车引擎关闭!
练习 2:鸭子类型计算器 (中)
设计一个简单的计算系统,包含 Adder、Multiplier 两个类。每个类都有 calculate(a, b) 方法。编写一个函数 perform_calculation(calculator, x, y),它应该能接受任何有 calculate 方法的对象。
要求:
Adder.calculate返回 a + bMultiplier.calculate返回 a * b- 函数不检查具体类型,只检查是否有
calculate方法
预期输出:
3 + 4 = 7
3 * 4 = 12
练习 3:插件系统 (难)
设计一个简单的"文件处理器"插件系统。有一个 process_file(filename, processor) 函数,processor 是一个对象。创建 TextProcessor、ImageProcessor 两个插件类,它们都有 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. 小结
关键要点回顾:
-
多态是面向对象的核心特性,允许同一接口在不同对象上表现不同行为。
-
Python 通过两种方式实现多态:
- 继承多态:基于类层次结构和方法重写。
- 鸭子类型:基于对象拥有的方法和属性,与继承无关。
-
鸭子类型是 Python 的哲学:"不检查是什么,只检查能做什么"。
-
最佳实践:
- 优先考虑鸭子类型,使代码更灵活
- 当需要强制接口时,使用抽象基类(ABC)
- 保持方法名和接口一致
- 适当使用
hasattr()进行安全检查
-
多态的好处:
- 代码更灵活,易于扩展
- 减少条件判断语句
- 提高代码复用性
- 支持"开闭原则"(对扩展开放,对修改关闭)
下一课预告:我们将学习 Python 的魔术方法(如 __str__, __repr__),这些特殊方法让你能够自定义类的行为,实现更高级的多态和操作符重载。