下一讲 — 第三模块
继承与层次结构
一个类如何继承另一个类,如何复用和专态化行为,以及如何在 UML 和 SysML v2 中建模泛化层次结构。
隐藏数据、设计清晰的公开接口,并引入书店领域的第三个类——ShoppingCart。
在第一模块中,Book 类的所有属性均为公开属性。程序中任何地方的代码都可以直接读写它们:
book1.price = -9.99 # 非法:价格不能为负 book1.stock = "lots" # 非法:库存必须是整数 book1.isbn = "" # 非法:ISBN 不能为空 # 对象现在处于错误状态。没有任何报错。
| 关切点 | 含义 |
|---|---|
| 公开接口 | 外部允许调用的操作。稳定、有意为之、有文档记录。 |
| 私有实现 | 内部数据与辅助逻辑。隐藏、可更改而不破坏调用方。 |
| 命名惯例 | 可见性 | 含义 |
|---|---|---|
name | 公开 | 类公开接口的一部分,可在任意地方安全使用。 |
_name | 受保护 | 内部使用,供类本身及其子类使用。不鼓励外部访问。 |
__name | 私有 | 触发名称改写。强烈表示:内部实现细节,请勿触动。 |
class Book: def __init__(self, isbn, title, author, price, stock=0): self.isbn = isbn # 公开 — 外部可直接读取 self.title = title self.author = author self._price = price # 受保护 — 建议通过 property 访问 self._stock = stock # 受保护 — 建议通过 property 访问
@property def price(self) -> float: return self._price @price.setter def price(self, value: float) -> None: if value < 0: raise ValueError(f"价格不能为负: {value}") self._price = value @property def stock(self) -> int: return self._stock @stock.setter def stock(self, value: int) -> None: if not isinstance(value, int) or value < 0: raise ValueError("库存必须是非负整数") self._stock = value
book1.price = 39.99 # 正常,通过 setter book1.price = -5.00 # ValueError: 价格不能为负: -5.0 book1.stock = "lots" # ValueError: 库存必须是非负整数
类不变量是对类的每个有效实例必须始终成立的条件。对于 Book 类:
price ≥ 0 — 书籍不能有负价格。stock ≥ 0 且为整数。isbn 非空 — 每本书必须有唯一标识符。def __init__(self, isbn: str, ...): if not isbn or not isbn.strip(): raise ValueError("ISBN 不能为空") self.isbn = isbn
领域的第三个类。它持有一组 (Book, 数量) 配对,展示了封装在一个包含可变集合的复杂类上的应用:
class ShoppingCart: """属于一个顾客的购物篮。""" def __init__(self, customer): self._customer = customer # 私有:拥有者 self._items: list[tuple] = [] # 私有:(Book, int) 列表 def add_item(self, book, quantity: int = 1) -> None: if quantity <= 0: raise ValueError("数量必须为正整数") if not book.is_available(): raise ValueError(f"{book.title} 已缺货") for i, (b, q) in enumerate(self._items): if b.isbn == book.isbn: self._items[i] = (b, q + quantity) return self._items.append((book, quantity)) def get_total(self) -> float: return sum(b.price * q for b, q in self._items) @property def items(self) -> list: return list(self._items) # 返回副本而非引用
items 返回 list(self._items)——内部列表的副本,而非其引用。如果直接返回 self._items,调用方可以在类外变更内部列表,绕过所有验证逻辑。
Book(带 property)与 ShoppingCart。«property» 构造型标注受控属性访问。一个方法只应调用以下对象的方法:对象自身、传入的参数、方法内部创建的对象,以及 self 的直接属性。只用一个点——不要穿越一个对象去调用另一个对象的方法。
# 差 — 违反迪米特法则(两跳:cart → book → str) for book, qty in cart.items: print(book.author.upper()) # 好 — 让 book 自己暴露所需信息 for book, qty in cart.items: print(book.get_details())
class Customer: def __init__(self, customer_id, name, email): self._customer_id = customer_id self._name = name self._email = "" self.email = email # 调用 setter 进行验证 @property def email(self): return self._email @email.setter def email(self, value): if "@" not in value: raise ValueError(f"无效的电子邮件地址: {value}") self._email = value def update_email(self, new_email): self.email = new_email # 通过 setter
Customer 拥有一个 ShoppingCart。标签"owns"与多重度(1 — 1..1)是标准 UML 关联记法。SysML v2 直接支持在特征(feature)上使用 UML 可见性标记。从 Python 惯例到 SysML v2 的映射非常直接:
| Python 惯例 | SysML v2 对应 |
|---|---|
name — 公开属性 | public attribute name : Type; |
_name — 受保护属性 | protected attribute _name : Type; |
__name — 私有属性 | private attribute __name : Type; |
+ 公开方法 | public action def method; |
− 私有方法 | private action def method; |
public attributeprotected attributeprivate attributederived attributeprivate 可见性将访问限制在拥有命名空间内(part def 块内)。protected 可见性允许专一化(子类)访问。这与 Python 的 _ 和 __ 惯例直接映射。
将公开接口与私有实现分离。外部代码只依赖接口,而非内部数据布局。
name 公开,_name 受保护,__name 私有。惯例为基础,所有严谨的 Python 代码均遵守。
为看似普通的属性赋值操作添加验证逻辑。需要执行不变量时使用。
每个有效实例任何时刻必须成立的条件。封装保证只有类自身的方法能改变其状态。
通过 property 暴露可变内部状态时,应返回副本而非引用,防止外部代码绕过验证直接修改私有数据。
只调用 self、参数、局部创建对象和直接属性的方法。用一个点,不用链式调用。
一个类如何继承另一个类,如何复用和专态化行为,以及如何在 UML 和 SysML v2 中建模泛化层次结构。