组合与聚合
对象如何由其他对象组装——深入探讨 has-a 关系。我们构建 Order、OrderItem 和 Payment,探讨组合(生命周期所有权)和聚合(共享引用)的区别,并将两者映射到 SysML v2 的 part 和 reference 语义。
一个接口,多种实现——同一个方法调用如何根据对象的类型产生不同的行为,从运行时分发到运算符重载。
多态(来自希腊语"多种形式")是不同对象对同一消息以各自方式响应的能力。你对不同对象调用相同的方法,每个对象根据自己的类型做出适当的事情——而调用代码不需要知道它正在处理哪种类型。
| 多态类型 | Python 的实现方式 |
|---|---|
| 子类型多态 | 在子类中重写方法。运行时类型决定运行哪个版本。 |
| 鸭子类型检测 | 任何实现了预期方法的对象都可以被使用,无论其类层次结构如何。 |
| 运算符重载 | 魔法方法(__add__、__lt__ 等)赋予标准运算符针对具体对象的含义。 |
Book 的函数不需要知道它处理的是 DigitalBook 还是 PhysicalBook。这使代码具有可扩展性:添加新的 Book 子类型时无需修改任何处理书籍的现有代码。
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 层次。两个子类都重写了 get_details();Book 还提供 __str__、__lt__、__eq__,所有子类型均自动继承比较和字符串行为。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(x, DigitalBook) | 需访问只有 DigitalBook 才有的方法(如 download())。谨慎使用。 |
isinstance(x, Book) | 需要确认对象至少是一本书。验证时可接受。 |
Python 使用魔法(dunder)方法将对象与语言本身集成。这些都是多态的形式——同一个内置函数根据接收的对象表现得不同:
# 添加到 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__ 面向开发者:应当足够精确以便重建对象。只实现一个时,首选 __repr__——Python 在 REPL 和容器(如列表)中将其用作 __str__ 的备用方案。
Python 允许你通过实现魔法方法来定义标准运算符(==、<、+、in)在自定义类型上的行为:
# 添加到 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 习语自然工作: 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。Book 获得 __str__、__repr__、__lt__、__eq__、__hash__;ShoppingCart 获得 __len__、__contains__、__iter__、__add__——使其表现得像原生 Python 序列。__eq__ 时,Python 会自动将 __hash__ 设为 None,使类变得不可哈希(无法用于集合或字典键)。定义 __eq__ 时必须同时定义 __hash__。安全的默认实现:return hash(self.isbn)。
多态的真正价值在于你可以编写函数,无需修改即可对任何现有或未来的 Book 子类型正确工作:
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 原则之一,第七模块将深入讲解。
在 SysML v2 中,多态自然地由第三模块介绍的子类化和重定义运算符产生。在 abstract part def 上声明的动作(方法)自动为多态的:
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..*];
abstract action def + :>> 重定义Book[*] 多重度操作的动作part def 上定义的行为自动适用于所有专态化。这让 SysML v2 模型可以表达系统能够处理满足接口的任何组件而无需对每个具体类型命名——直接类似于建模层面的鸭子类型检测。
Python 在运行时对对象的实际类型解析方法调用——而非变量的声明类型。
任何具有正确方法的对象都可以多态使用。无需共享基类。最灵活的形式。
__str__ 面向用户,__repr__ 面向开发者。只实现一个时,首选 __repr__。
定义 __eq__ 时必须同时定义 __hash__,以保持对象在集合和字典中的可用性。
多态函数对扩展开放,对修改封闭。添加新子类型时不需更改现有代码。
通过 abstract action def + :>> 重定义实现多态。part catalogue : Book[*] 是多态集合。
对象如何由其他对象组装——深入探讨 has-a 关系。我们构建 Order、OrderItem 和 Payment,探讨组合(生命周期所有权)和聚合(共享引用)的区别,并将两者映射到 SysML v2 的 part 和 reference 语义。