Ran Wei / 面向对象设计 / 第四模块
EN
面向对象设计 — Ran Wei

第四模块:多态

一个接口,多种实现——同一个方法调用如何根据对象的类型产生不同的行为,从运行时分发到运算符重载。

预备知识:OOD 第三模块 Python 3 鸭子类型 魔法方法 开放/封闭原则 SysML v2 对应
1

什么是多态?

🏠 生活类比
想象一个遥控器:无论你按下"播放"按钮,电视、音响、播放器进行的操作各不相同。根据具体设备的类型,同一命令产生不同的输出。这就是多态——同一消息,多种响应。

多态(来自希腊语"多种形式")是不同对象对同一消息以各自方式响应的能力。你对不同对象调用相同的方法,每个对象根据自己的类型做出适当的事情——而调用代码不需要知道它正在处理哪种类型。

多态类型Python 的实现方式
子类型多态在子类中重写方法。运行时类型决定运行哪个版本。
鸭子类型检测任何实现了预期方法的对象都可以被使用,无论其类层次结构如何。
运算符重载魔法方法(__add____lt__ 等)赋予标准运算符针对具体对象的含义。
ℹ️ 核心洞察
多态将调用代码与具体类型解耦。接受 Book 的函数不需要知道它处理的是 DigitalBook 还是 PhysicalBook。这使代码具有可扩展性:添加新的 Book 子类型时无需修改任何处理书籍的现有代码。
2

方法重写再讨论

Python 调用方法时,它在运行时对对象的实际类型进行方法查找——而非变量声明的类型。这称为动态分发

Python
catalogue: list[Book] = [
    DigitalBook("978-1-11", "清洁代码",   "Martin",  29.99, "PDF",  5.2, "..."),
    PhysicalBook("978-1-22", "重构",      "Fowler",  39.99, 5, 520, "...", "WH1"),
    DigitalBook("978-1-33", "务实程序员", "Hunt",    34.99, "EPUB", 8.4, "..."),
]

# 多态循环:相同调用,每种类型产生不同行为
for book in catalogue:
    print(book.get_details())            # 自动分发到正确子类
    print(f"  有货:{book.is_available()}")  # 继承方法——两种均相同
带有多态方法的 Book 层次
图 1 — 带有多态方法的 Book 层次。两个子类都重写了 get_details()Book 还提供 __str____lt____eq__,所有子类型均自动继承比较和字符串行为。
3

鸭子类型检测

🏠 生活类比
想象一个小吃部:只要你能坐位就座、能点餐、能用餐具,就可以进餐——无论你是商务人士还是学生。鸭子类型检测也是如此:只要对象能执行需要的操作,就能被使用,无论它属于哪个类。

Python 不要求对象共享公共基类才能多态使用。任何具有正确方法的对象都可以被使用。这就是鸭子类型检测:

Python
# 一个完全独立的类——不是 Book 子类
class GiftCard:
    def __init__(self, code: str, value: float):
        self.code = code; self.value = value; self.stock = 1

    def get_details(self) -> str:
        return f"礼品卡 {self.code} | £{self.value:.2f}"

    def is_available(self) -> bool:
        return self.stock > 0


# print_catalogue 只关心对象是否有 get_details() 和 is_available()
def print_catalogue(items) -> None:
    for item in items:
        print(item.get_details())
        if not item.is_available():
            print("  *** 缺货 ***")


mixed = [
    DigitalBook("978-1-11", "清洁代码", "Martin", 29.99, "PDF", 5.2, "..."),
    GiftCard("GC-2024-001", 25.00),
]
print_catalogue(mixed)   # 适用于两者——无需继承
💡 鸭子类型检测
print_catalogue() 适用于任何具有 get_details()is_available() 的对象,跟类层次无关。这是 Python 中最灵活的多态形式,让函数与具体类型解耦,并且容易用简单的桩对象进行测试。

isinstance() 与鸭子类型检测

模式使用时机
鸭子类型(直接调用方法)只需要公共接口。首选默认。
isinstance(x, DigitalBook)需访问只有 DigitalBook 才有的方法(如 download())。谨慎使用。
isinstance(x, Book)需要确认对象至少是一本书。验证时可接受。
4

魔法方法:__str__、__repr__、__len__

Python 使用魔法(dunder)方法将对象与语言本身集成。这些都是多态的形式——同一个内置函数根据接收的对象表现得不同:

Python
# 添加到 Book
def __str__(self) -> str:
    return self.get_details()   # 委托给被重写的方法

def __repr__(self) -> str:
    return (f"{self.__class__.__name__}(isbn={self.isbn!r}, "
            f"title={self.title!r}, price={self.price!r})")


# 添加到 ShoppingCart
def __len__(self) -> int:    return len(self._items)
def __contains__(self, b): return b in self._items
def __iter__(self):         return iter(self._items)


# 使用
book = DigitalBook("978-1-11", "清洁代码", "Martin", 29.99, "PDF", 5.2, "...")
print(book)          # "清洁代码 作者:Martin | PDF (5.2 MB) £29.99"
repr(book)           # "DigitalBook(isbn='978-1-11', title='清洁代码', ...)"
print(len(cart))     # 1
print(book in cart)  # True
ℹ️ __str__ 与 __repr__
__str__ 面向用户:可读且简洁。__repr__ 面向开发者:应当足够精确以便重建对象。只实现一个时,首选 __repr__——Python 在 REPL 和容器(如列表)中将其用作 __str__ 的备用方案。
5

运算符重载

Python 允许你通过实现魔法方法来定义标准运算符(==<+in)在自定义类型上的行为:

Python
# 添加到 Book
def __eq__(self, other) -> bool:
    if not isinstance(other, Book): return NotImplemented
    return self.isbn == other.isbn

def __hash__(self) -> int:   # 定义 __eq__ 时必须提供
    return hash(self.isbn)

def __lt__(self, other) -> bool:
    if not isinstance(other, Book): return NotImplemented
    return self.price < other.price

# ShoppingCart 合并
def __add__(self, other):
    if not isinstance(other, ShoppingCart): return NotImplemented
    merged = ShoppingCart(f"{self.cart_id}+{other.cart_id}")
    for b in self: merged.add_item(b)
    for b in other: merged.add_item(b)
    return merged
Python
# 现在标准 Python 习语自然工作:

books.sort()          # 使用 __lt__ 按价格排序
for b in books: print(f"  £{b.price:.2f}  {b.title}")
# £24.99  SICP
# £29.99  清洁代码
# £39.99  重构

b1 = DigitalBook("978-1-11", "清洁代码", "Martin", 29.99, "PDF",  5.2, "...")
b2 = DigitalBook("978-1-11", "清洁代码", "Martin", 29.99, "EPUB", 6.0, "...")
print(b1 == b2)       # True — 相同的 ISBN,不同格式

unique_books = {b1, b2}
print(len(unique_books))  # 1(需要 __hash__)
带有运算符重载的 Book 和 ShoppingCart
图 2 — 带有运算符重载的 BookShoppingCartBook 获得 __str____repr____lt____eq____hash__ShoppingCart 获得 __len____contains____iter____add__——使其表现得像原生 Python 序列。
⚠️ 注意——忘记 __hash__
定义 __eq__ 时,Python 会自动将 __hash__ 设为 None,使类变得不可哈希(无法用于集合或字典键)。定义 __eq__ 时必须同时定义 __hash__安全的默认实现:return hash(self.isbn)
6

编写多态函数

多态的真正价值在于你可以编写函数,无需修改即可对任何现有或未来的 Book 子类型正确工作:

Python
from typing import Sequence

# 1. 对任意书籍集合应用折扣
def apply_sale(books: Sequence[Book], discount: float) -> None:
    for book in books:
        book.apply_discount(discount)
        print(f"  {book.title} -> £{book.price:.2f}")


# 2. 将混合目录分为数字和实体两类
def split_catalogue(books):
    digital  = [b for b in books if isinstance(b, DigitalBook)]
    physical = [b for b in books if isinstance(b, PhysicalBook)]
    return digital, physical


# 3. 计算实体书的总运费
def total_shipping(books) -> float:
    return sum(
        b.get_shipping_cost()
        for b in books
        if isinstance(b, PhysicalBook)
    )


apply_sale(catalogue, 15)       # 全部打八五折
digital, physical = split_catalogue(catalogue)
print(f"运费:£{total_shipping(physical):.2f}")
💡 开放/封闭原则
多态函数如 apply_sale() 对扩展开放(添加 AudioBook 时自动适用)、对修改封闭(添加 AudioBook 时不需修改 apply_sale())。这就是开放/封闭原则,SOLID 原则之一,第七模块将深入讲解。
7

SysML v2 对应

在 SysML v2 中,多态自然地由第三模块介绍的子类化和重定义运算符产生。在 abstract part def 上声明的动作(方法)自动为多态的:

SysML v2
abstract part def Book {
    abstract action def get_details : String;  // 多态
    action def is_available : Boolean;
}

part def DigitalBook :> Book {
    :>> get_details : String;   // 重定义——运行时分发
}

part def PhysicalBook :> Book {
    :>> get_details : String;   // 不同的重定义
}

// catalogue 容纳任意 Book 子类型——多态集合
part catalogue : Book[0..*];
Python / OOD
SysML v2
多态方法调用
abstract action def + :>> 重定义
鸭子类型(结构化)
结构一致性/分类器兼容性
isinstance(x, Book)
通过子类化链检查类型成员关系
多态函数
Book[*] 多重度操作的动作
ℹ️ MBSE 中的多态
在基于模型的系统工程学中,多态通过专态化层次自然表达。在抽象块或 part def 上定义的行为自动适用于所有专态化。这让 SysML v2 模型可以表达系统能够处理满足接口的任何组件而无需对每个具体类型命名——直接类似于建模层面的鸭子类型检测。
8

总结

动态分发

Python 在运行时对对象的实际类型解析方法调用——而非变量的声明类型。

鸭子类型

任何具有正确方法的对象都可以多态使用。无需共享基类。最灵活的形式。

__str__ / __repr__

__str__ 面向用户,__repr__ 面向开发者。只实现一个时,首选 __repr__

__eq__ + __hash__

定义 __eq__ 时必须同时定义 __hash__,以保持对象在集合和字典中的可用性。

开放/封闭

多态函数对扩展开放,对修改封闭。添加新子类型时不需更改现有代码。

SysML v2

通过 abstract action def + :>> 重定义实现多态。part catalogue : Book[*] 是多态集合。

🧱
下一讲 — 第五模块

组合与聚合

对象如何由其他对象组装——深入探讨 has-a 关系。我们构建 OrderOrderItemPayment,探讨组合(生命周期所有权)和聚合(共享引用)的区别,并将两者映射到 SysML v2 的 partreference 语义。