多态
同一个方法调用如何根据对象的类型产生不同的行为、Python 中的鸭子类型、运算符重载,以及 UML 和 SysML v2 中的多态全景。
通过类层次结构复用和专态化行为——将 Book 扩展为 DigitalBook 和 PhysicalBook,并引入抽象类。
继承建模"is-a"关系。DigitalBook 是一种 Book。PremiumCustomer 是一种 Customer。子类继承父类的全部属性和方法,并可在此基础上添加自己的内容,或修改继承来的方法。
| 关系类型 | 使用时机 |
|---|---|
| is-a(是一种) | 继承。DigitalBook is-a Book——共享所有 Book 行为并添加自己的运作。 |
| has-a(拥有) | 组合。ShoppingCart has Books——它持有 Book 对象的引用,但本身不是 Book。 |
我们的书店销售两种书:数字下载和纸质印刷。两种都共享核心 Book 属性,但各有其独特的额外属性。继承让我们无需复制任何代码就能表达这种关系:
from abc import ABC, abstractmethod class Book(ABC): """所有书籍类型的抽象基类。""" def __init__(self, isbn, title, author, price, stock=0): if not isbn.strip(): raise ValueError("ISBN 不能为空") self.isbn = isbn; self.title = title; self.author = author self._price = 0.0; self._stock = 0 self.price = price; self.stock = stock @abstractmethod def get_details(self) -> str: """每个子类必须提供自己的描述。""" ... def is_available(self): return self.stock > 0 class DigitalBook(Book): """可下载的电子书。""" def __init__(self, isbn, title, author, price, file_format, file_size, download_url): super().__init__(isbn, title, author, price, stock=999) self.file_format = file_format # 如 "EPUB"、"PDF" self.file_size = file_size # 单位:MB self.download_url = download_url def get_details(self) -> str: return f"{self.title} 作者:{self.author} | {self.file_format} ({self.file_size:.1f} MB) £{self.price:.2f}" def download(self): print(f"下载自 {self.download_url}") class PhysicalBook(Book): """存放在仓库中的实体书籍。""" SHIPPING_RATE = 0.005 # 英镑/克 def __init__(self, isbn, title, author, price, stock, weight, dimensions, warehouse): super().__init__(isbn, title, author, price, stock) self.weight = weight; self.dimensions = dimensions self.warehouse = warehouse def get_details(self) -> str: return f"{self.title} 作者:{self.author} | 纸质书 £{self.price:.2f} ({self.weight}克)" def get_shipping_cost(self) -> float: return round(self.weight * self.SHIPPING_RATE, 2)
Book 与两个具体子类。空心箭头表示泛化(继承)。«abstract» 标注基类和抽象方法。digital = DigitalBook( isbn="978-0-13-468599-1", title="务实程序员", author="Hunt & Thomas", price=34.99, file_format="EPUB", file_size=8.4, download_url="https://books.example.com/pp.epub" ) print(digital.isbn) # "978-0-13-468599-1" print(digital.is_available()) # True(stock=999 由 DigitalBook.__init__ 设置) print(digital.get_details()) # "务实程序员 作者:Hunt & Thomas | EPUB (8.4 MB) £34.99" digital.download() # "下载自 https://..." digital.apply_discount(10) print(digital.price) # 31.491
super() 并不意味着"直接调用父类",而是调用方法解析顺序(MRO)中的下一个类。对于单继承,这永远是直接父类,但在多重继承时这个区别至关重要。
DigitalBook 和 PhysicalBook 都重写了 get_details()。方法名称相同,实现不同。Python 在运行时根据对象的实际类型自动选择正确版本:
books = [ DigitalBook("978-1-11", "清洁代码", "Martin", 29.99, "PDF", 5.2, "..."), PhysicalBook("978-1-22", "重构", "Fowler", 39.99, 5, 520, "...", "WH1"), ] for book in books: print(book.get_details()) # 自动调用正确版本 # 清洁代码 作者:Martin | PDF (5.2 MB) £29.99 # 重构 作者:Fowler | 纸质书 £39.99 (520克)
这就是多态——同一个方法调用根据对象的类型产生不同的行为。第四模块将深入讲解多态。
Book 的代码必须同样适用于 DigitalBook 或 PhysicalBook。
抽象类无法直接实例化——它只存在于被继承。@abstractmethod 强制每个具体子类提供自己的实现:
# 无法实例化抽象类: book = Book("978-0", "测试", "作者", 9.99) # TypeError: 无法实例化抽象类 Book # 它有未实现的抽象方法 get_details # 忘记实现 get_details 的子类同样失败: class BrokenBook(Book): pass broken = BrokenBook("978-0", "测试", "作者", 9.99) # TypeError: 无法实例化抽象类 BrokenBook
@abstractmethod 是一个合同:"每个具体 Book 都必须知道如何描述自己。"抽象类定义接口,每个子类提供实现。这是多态的基础。
class PremiumCustomer(Customer): """参与积分计划的顾客。""" def __init__(self, customer_id, name, email, discount_rate=0.10): super().__init__(customer_id, name, email) if not 0 <= discount_rate <= 1: raise ValueError("折扣率必须在0到1之间") self._loyalty_points = 0 self._discount_rate = discount_rate def add_points(self, points: int): if points <= 0: raise ValueError("积分必须为正整数") self._loyalty_points += points def get_details(self) -> str: return (f"{self.get_name()} (高级会员) | " f"积分:{self._loyalty_points} | " f"折扣:{self._discount_rate*100:.0f}%")
Customer 与 PremiumCustomer。空心箭头表示继承。PremiumCustomer 继承所有 Customer 属性和方法,并添加积分计划行为。继承功能强大但常被滥用。一个有用的经验法则:除非明确满足 is-a 测试且不太可能改变,否则首选组合而非继承。
| 因素 | 倾向继承 | 倾向组合 |
|---|---|---|
| 关系 | "Is-a":DigitalBook 是一种 Book | "Has-a":ShoppingCart 拥有 Books |
| 灵活性 | 层次稳定,不大可能改变 | 行为可能在运行时变化或重配置 |
| 耦合度 | 紧耦合可接受——子类了解父类 | 较松耦合——组件相互独立 |
SysML v2 为继承提供了直接的语言构造,映射非常精确:
| Python / OOD | SysML v2 |
|---|---|
class Child(Parent) | part def Child :> Parent { ... } |
super().__init__() | 隐式——继承特征自动可用 |
| 重写方法 | :>> methodName { ... } |
| 抽象类(ABC) | abstract part def Book { ... } |
@abstractmethod | abstract action def get_details : String; |
part def Child :> Parent:>> methodNameabstract part defabstract action def:>(子类化)声明"此 part def 是那个的专态化"——等价于类继承。:>>(重定义)重写父类中的某个具体特征——等价于方法重写。两者配合使用::> 建立层次,:>> 在层次内定制具体特征。
只有当子类真正是父类的专态化且满足里氏替换原则时,才使用继承。
子类构造函数中必须调用 super().__init__(),确保父类在添加子类状态之前被完整初始化。
每个子类提供自己的实现。Python 在运行时自动选择正确版本。合同必须被保留。
ABC + @abstractmethod 定义所有具体子类必须实现的接口。无法直接实例化。
当 is-a 关系不明确、层次可能很深或需要运行时灵活性时,首选组合而非继承。
:> 声明子类化,:>> 重定义特征。abstract part def 和 abstract action def 强制执行抽象类模式。
同一个方法调用如何根据对象的类型产生不同的行为、Python 中的鸭子类型、运算符重载,以及 UML 和 SysML v2 中的多态全景。