feat(面向对象): 完成第三阶段差异化课程
This commit is contained in:
266
03_面向对象/3_4_Python特殊方法与数据类/README.md
Normal file
266
03_面向对象/3_4_Python特殊方法与数据类/README.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 第 3-4 课:Python 特殊方法与数据类
|
||||
|
||||
## 一、本课定位
|
||||
|
||||
Java 业务对象中经常需要构造器、`toString()`、`equals()`、`hashCode()`,项目也常借助 record 或 Lombok 减少样板代码。Python 通过特殊方法和数据类(Data Class,`@dataclass`)解决类似问题。
|
||||
|
||||
本课只选择业务开发中最常见的内容,不展开运算符重载的全部细节。
|
||||
|
||||
## 二、本课目标
|
||||
|
||||
完成本课后,你将能够:
|
||||
|
||||
1. 理解 Python 特殊方法的调用机制;
|
||||
2. 区分 `__str__` 和 `__repr__`;
|
||||
3. 理解 `__eq__` 如何改变对象相等比较;
|
||||
4. 使用 `@dataclass` 减少数据对象的样板代码;
|
||||
5. 使用 `__post_init__` 执行业务校验;
|
||||
6. 使用 `field(default_factory=list)` 避免共享可变默认值;
|
||||
7. 理解 dataclass 与 Java POJO、record、Lombok 的边界差异。
|
||||
|
||||
## 三、差异速查表
|
||||
|
||||
| 需求 | Java 常见方式 | Python 常见方式 |
|
||||
|---|---|---|
|
||||
| 用户友好显示 | `toString()` | `__str__` |
|
||||
| 开发调试表示 | 通常仍使用 `toString()` | `__repr__` |
|
||||
| 内容相等 | 重写 `equals()` | 重写 `__eq__` |
|
||||
| 减少数据类样板代码 | record 或 Lombok | `@dataclass` |
|
||||
| 构造后校验 | 构造器代码块 | `__post_init__` |
|
||||
| 每个对象独立列表 | 构造时 `new ArrayList<>()` | `field(default_factory=list)` |
|
||||
|
||||
## 四、特殊方法是什么
|
||||
|
||||
名称前后都有双下划线的方法称为特殊方法(Special Method),也常被称为双下方法(Dunder Method)。
|
||||
|
||||
通常不是直接调用:
|
||||
|
||||
```python
|
||||
item.__str__()
|
||||
```
|
||||
|
||||
而是使用正常语法,让 Python 自动调用:
|
||||
|
||||
```python
|
||||
str(item)
|
||||
print(item)
|
||||
item_one == item_two
|
||||
```
|
||||
|
||||
这与 Java 运行时在字符串拼接或打印对象时调用 `toString()` 的体验相似。
|
||||
|
||||
## 五、__str__ 与 __repr__
|
||||
|
||||
`__str__` 面向使用者,强调友好、易读:
|
||||
|
||||
```python
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}:{self.price} 元"
|
||||
```
|
||||
|
||||
`__repr__` 面向开发和调试,通常包含类名和明确字段:
|
||||
|
||||
```text
|
||||
Product(name='机械键盘', price=500.0)
|
||||
```
|
||||
|
||||
调用关系:
|
||||
|
||||
- `str(obj)` 和 `print(obj)` 优先使用 `__str__`;
|
||||
- `repr(obj)`、交互环境和容器显示通常使用 `__repr__`;
|
||||
- 如果没有定义 `__str__`,Python 可能退回使用 `__repr__`。
|
||||
|
||||
## 六、__eq__ 与对象相等
|
||||
|
||||
普通类如果没有实现 `__eq__`,两个字段内容相同但身份不同的对象通常不相等:
|
||||
|
||||
```python
|
||||
first = Product("键盘", 500.0)
|
||||
second = Product("键盘", 500.0)
|
||||
```
|
||||
|
||||
实现 `__eq__` 后,可以按照业务字段比较。`@dataclass` 默认会根据声明的全部字段自动生成 `__eq__`。
|
||||
|
||||
注意:相等比较与对象身份判断不同:
|
||||
|
||||
- `first == second` 调用相等逻辑;
|
||||
- `first is second` 判断是否为同一个对象。
|
||||
|
||||
## 七、@dataclass 生成什么
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Product:
|
||||
name: str
|
||||
price: float
|
||||
```
|
||||
|
||||
默认会自动生成常用方法,包括:
|
||||
|
||||
- `__init__`:接收字段并初始化;
|
||||
- `__repr__`:生成适合调试的表示;
|
||||
- `__eq__`:按照字段值比较。
|
||||
|
||||
因此一般不要再手写完全相同的样板方法。
|
||||
|
||||
## 八、dataclass 不是什么
|
||||
|
||||
`@dataclass` 并不会:
|
||||
|
||||
- 自动进行运行时类型检查;
|
||||
- 自动完成业务规则校验;
|
||||
- 默认创建不可变对象;
|
||||
- 自动等同于 Java record;
|
||||
- 自动替代数据库对象关系映射(Object Relational Mapping,ORM)模型。
|
||||
|
||||
例如,`price: float` 只是类型提示,不能自动阻止负数或字符串。
|
||||
|
||||
## 九、使用 __post_init__ 校验
|
||||
|
||||
dataclass 自动生成 `__init__` 后会调用 `__post_init__`:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Product:
|
||||
name: str
|
||||
price: float
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.price < 0:
|
||||
raise ValueError("价格不能小于 0。")
|
||||
```
|
||||
|
||||
它适合执行字段间校验、格式整理或需要在初始化完成后执行的逻辑。
|
||||
|
||||
## 十、可变默认值与 default_factory
|
||||
|
||||
列表是可变对象。下面这种字段定义不应使用:
|
||||
|
||||
```python
|
||||
items: list[Product] = []
|
||||
```
|
||||
|
||||
dataclass 会直接拒绝常见的可变默认值,避免多个实例意外共享同一列表。
|
||||
|
||||
正确写法:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class Cart:
|
||||
items: list[Product] = field(default_factory=list)
|
||||
```
|
||||
|
||||
`default_factory=list` 表示每次创建 `Cart` 时调用 `list()`,得到新的独立列表。
|
||||
|
||||
## 十一、与 Java record 和 Lombok 的区别
|
||||
|
||||
可以建立近似理解,但不要认为它们完全相同:
|
||||
|
||||
- dataclass 默认可变,Java record 的组件引用不可重新赋值;
|
||||
- dataclass 可以写普通方法、继承和自定义特殊方法;
|
||||
- dataclass 是 Python 标准库功能,不需要额外依赖或编译期注解处理;
|
||||
- `@dataclass(frozen=True)` 可以限制字段重新赋值,但仍不是安全边界,也不是深层不可变。
|
||||
|
||||
本阶段先掌握默认 dataclass,`frozen=True` 按业务需要再使用。
|
||||
|
||||
## 十二、什么时候使用 dataclass
|
||||
|
||||
适合:
|
||||
|
||||
- 主要职责是保存数据;
|
||||
- 需要清晰的字段定义;
|
||||
- 希望自动获得初始化、调试显示和内容比较;
|
||||
- 对象仍然包含少量与数据紧密相关的行为。
|
||||
|
||||
不应仅因为“这是一个类”就添加 dataclass。复杂服务对象、资源管理器或主要依靠行为工作的对象通常使用普通类更自然。
|
||||
|
||||
## 十三、完整示例与运行方法
|
||||
|
||||
示例文件:
|
||||
|
||||
```text
|
||||
03_面向对象/3_4_Python特殊方法与数据类/special_methods_dataclass_example.py
|
||||
```
|
||||
|
||||
在项目根目录运行:
|
||||
|
||||
```powershell
|
||||
python .\03_面向对象\3_4_Python特殊方法与数据类\special_methods_dataclass_example.py
|
||||
```
|
||||
|
||||
示例会展示普通类与 dataclass 的代码差异、字符串显示、字段相等、初始化校验和独立列表。
|
||||
|
||||
## 十四、常见错误
|
||||
|
||||
### 14.1 把 __repr__ 只当作另一个 __str__
|
||||
|
||||
二者用途不同:一个偏向开发调试,一个偏向使用者阅读。
|
||||
|
||||
### 14.2 认为类型注解会自动校验
|
||||
|
||||
`price: float` 不会自动拒绝字符串或负数。外部数据与业务规则仍需显式校验。
|
||||
|
||||
### 14.3 手写 dataclass 已生成的方法
|
||||
|
||||
如果自动行为已经符合需求,不要重复编写 `__init__`、`__repr__` 和 `__eq__`。
|
||||
|
||||
### 14.4 共享可变默认值
|
||||
|
||||
列表、字典和集合等可变默认值应使用 `default_factory` 创建。
|
||||
|
||||
### 14.5 默认认为 dataclass 可以哈希
|
||||
|
||||
可变 dataclass 通常不会自动提供可用的 `__hash__`。是否可作为字典键或集合元素涉及可变性和相等契约,本课不强行展开。
|
||||
|
||||
## 十五、课堂练习
|
||||
|
||||
打开:
|
||||
|
||||
```text
|
||||
03_面向对象/3_4_Python特殊方法与数据类/practice.py
|
||||
```
|
||||
|
||||
完成订单项和购物车:
|
||||
|
||||
1. 使用 dataclass 定义字段;
|
||||
2. 使用 `__post_init__` 校验单价和数量;
|
||||
3. 使用 property 计算小计;
|
||||
4. 使用 `__str__` 提供友好显示;
|
||||
5. 验证自动生成的 `__repr__` 和 `__eq__`;
|
||||
6. 使用 `default_factory=list` 创建独立购物车列表。
|
||||
|
||||
## 十六、参考答案与验证标准
|
||||
|
||||
参考答案暂不写入练习文件。完成后按以下层级验证:
|
||||
|
||||
- 必须修复:语法或运行错误、计算错误、校验缺失、共享列表、手写并破坏 dataclass 自动行为;
|
||||
- 建议改进:重复计算、职责或命名明显不清晰;
|
||||
- 可选优化:非必要类型注解、输出美化和不影响行为的格式问题。
|
||||
|
||||
只有“必须修复”会阻止课程验收。
|
||||
|
||||
## 十七、本课小结
|
||||
|
||||
- 特殊方法让对象参与 Python 的标准语法;
|
||||
- `__str__` 面向使用者,`__repr__` 面向开发调试;
|
||||
- `__eq__` 决定 `==` 的相等逻辑;
|
||||
- `@dataclass` 自动生成初始化、调试表示和字段比较;
|
||||
- dataclass 类型注解不会自动执行运行时校验;
|
||||
- `__post_init__` 适合完成初始化后的业务校验;
|
||||
- `default_factory` 为每个实例创建独立的可变字段。
|
||||
|
||||
## 十八、验收标准
|
||||
|
||||
- 能正确使用 `@dataclass`;
|
||||
- 能解释 `__str__` 与 `__repr__` 的区别;
|
||||
- 能观察自动生成的 `__eq__` 行为;
|
||||
- 能使用 `__post_init__` 阻止非法数据;
|
||||
- 能使用 `field(default_factory=list)`;
|
||||
- 能说明 dataclass 与 Java record 并非完全等价;
|
||||
- `practice.py` 的购物车计算与独立列表验证正确。
|
||||
87
03_面向对象/3_4_Python特殊方法与数据类/practice.py
Normal file
87
03_面向对象/3_4_Python特殊方法与数据类/practice.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# 第 3-4 课课堂练习:Python 特殊方法与数据类
|
||||
#
|
||||
# 关键规则已经写入每部分题目,可以直接完成练习。
|
||||
# 请保留题目、预期结果和自查说明。
|
||||
|
||||
|
||||
# 第一部分:定义 OrderItem 数据类
|
||||
# 1. 在类上方添加 @dataclass。
|
||||
# 2. 定义三个字段,不需要手写 __init__:
|
||||
# product_name: str
|
||||
# unit_price: float
|
||||
# quantity: int = 1
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - @dataclass 会生成 __init__、__repr__ 和 __eq__ 等常用方法;
|
||||
# - 它类似减少 POJO 样板代码的工具,但不是 Java record 的完全等价物;
|
||||
# - 数据类默认仍然可变,并不会自动进行运行时类型检查。
|
||||
|
||||
|
||||
# 第二部分:使用 __post_init__ 校验字段
|
||||
# 1. 定义 __post_init__(self) 方法。
|
||||
# 2. unit_price 小于 0 时抛出 ValueError("单价不能小于 0。")。
|
||||
# 3. quantity 小于或等于 0 时抛出 ValueError("数量必须大于 0。")。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - dataclass 自动生成 __init__ 后,会自动调用 __post_init__;
|
||||
# - 类型注解只表达预期类型,不会阻止负数,所以业务校验仍需手写。
|
||||
|
||||
|
||||
# 第三部分:定义 subtotal 只读 property
|
||||
# 1. 使用 @property 定义 subtotal(self),返回单价乘数量。
|
||||
# 2. 调用方式应为 item.subtotal,而不是 item.get_subtotal()。
|
||||
|
||||
|
||||
# 第四部分:定义 __str__(self)
|
||||
# 1. 返回“机械键盘 × 2:1000.0 元”格式的使用者友好文字。
|
||||
# 2. 使用 self.subtotal 取得小计。
|
||||
#
|
||||
# 特殊方法提醒:
|
||||
# - print(item) 和 str(item) 会调用 __str__;
|
||||
# - repr(item) 会使用 dataclass 自动生成的 __repr__;
|
||||
# - item_one == item_two 会使用自动生成的 __eq__ 比较全部字段。
|
||||
|
||||
|
||||
# 第五部分:定义 ShoppingCart 数据类
|
||||
# 1. 在类上方添加 @dataclass。
|
||||
# 2. 定义 owner: str 字段。
|
||||
# 3. 定义 items 字段:
|
||||
# items: list[OrderItem] = field(default_factory=list)
|
||||
# 4. 定义 add(self, item) 方法,把 item 添加到 self.items。
|
||||
# 5. 定义 total 只读 property,返回所有 item.subtotal 的总和。
|
||||
#
|
||||
# 重要提醒:
|
||||
# - 不要写 items = [] 作为类级默认列表;
|
||||
# - default_factory=list 会为每个 ShoppingCart 对象创建独立列表;
|
||||
# - 这与 Java 实例字段通常在构造时 new ArrayList<>() 的目的相似。
|
||||
|
||||
|
||||
# 第六部分:定义 main() 并验证行为
|
||||
# 1. 创建两个内容相同的 OrderItem("机械键盘", 500.0, 2)。
|
||||
# 2. 输出第一个对象,预期调用 __str__。
|
||||
# 3. 输出 repr(item_one),观察自动生成的调试表示。
|
||||
# 4. 输出“两个订单项相等:True”。
|
||||
# 5. 创建 ShoppingCart("小明") 和 ShoppingCart("小红")。
|
||||
# 6. 只向小明的购物车添加第一个订单项。
|
||||
# 7. 输出两个购物车的商品种类数,证明列表没有共享。
|
||||
# 8. 输出小明购物车的总金额。
|
||||
# 9. 使用 try...except 创建 OrderItem("错误商品", -1.0),捕获并输出错误。
|
||||
# 10. 添加程序入口判断并调用 main()。
|
||||
#
|
||||
# 预期输出:
|
||||
# 机械键盘 × 2:1000.0 元
|
||||
# OrderItem(product_name='机械键盘', unit_price=500.0, quantity=2)
|
||||
# 两个订单项相等:True
|
||||
# 小明的商品种类数:1
|
||||
# 小红的商品种类数:0
|
||||
# 小明购物车总金额:1000.0 元
|
||||
# 非法订单项已被阻止:单价不能小于 0。
|
||||
|
||||
|
||||
# 完成后自查:
|
||||
# 1. 是否没有为 OrderItem 手写 __init__、__repr__ 和 __eq__;
|
||||
# 2. 是否能区分 __str__ 的用户显示与 __repr__ 的调试表示;
|
||||
# 3. 是否知道 dataclass 的相等比较默认比较字段值;
|
||||
# 4. 是否使用 __post_init__ 完成运行时业务校验;
|
||||
# 5. 是否使用 default_factory=list 避免共享可变默认值;
|
||||
# 6. 是否只把运行错误、结果错误和关键知识点错误视为阻塞项。
|
||||
@@ -0,0 +1,94 @@
|
||||
# 第 3-4 课完整示例:Python 特殊方法与数据类
|
||||
#
|
||||
# 本示例对比普通类和 @dataclass,并演示 __str__、自动 __repr__、
|
||||
# 自动 __eq__、__post_init__ 以及 field(default_factory=list)。
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
class ManualProduct:
|
||||
"""手动实现初始化、字符串显示和相等比较的普通类。"""
|
||||
|
||||
def __init__(self, name: str, price: float) -> None:
|
||||
self.name = name
|
||||
self.price = price
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""返回适合开发和调试的对象表示。"""
|
||||
return f"ManualProduct(name={self.name!r}, price={self.price!r})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""根据属性值判断两个商品是否相等。"""
|
||||
if not isinstance(other, ManualProduct):
|
||||
return NotImplemented
|
||||
return self.name == other.name and self.price == other.price
|
||||
|
||||
|
||||
@dataclass
|
||||
class Product:
|
||||
"""数据类会根据字段自动生成常用特殊方法。"""
|
||||
|
||||
name: str
|
||||
price: float
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""在自动生成的 __init__ 执行后校验数据。"""
|
||||
if self.price < 0:
|
||||
raise ValueError("价格不能小于 0。")
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""返回面向使用者的友好文字。"""
|
||||
return f"{self.name}:{self.price} 元"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Cart:
|
||||
"""购物车使用工厂函数为每个实例创建独立列表。"""
|
||||
|
||||
owner: str
|
||||
items: list[Product] = field(default_factory=list)
|
||||
|
||||
def add(self, product: Product) -> None:
|
||||
"""向当前购物车添加商品。"""
|
||||
self.items.append(product)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""运行特殊方法与数据类示例。"""
|
||||
manual_one = ManualProduct("机械键盘", 500.0)
|
||||
manual_two = ManualProduct("机械键盘", 500.0)
|
||||
|
||||
print("差异 1:普通类需要手动实现 Java 常见样板能力。")
|
||||
print(repr(manual_one))
|
||||
print(f"两个普通类对象按内容相等:{manual_one == manual_two}")
|
||||
print()
|
||||
|
||||
keyboard_one = Product("机械键盘", 500.0)
|
||||
keyboard_two = Product("机械键盘", 500.0)
|
||||
|
||||
print("差异 2:@dataclass 自动生成 __init__、__repr__ 和 __eq__。")
|
||||
print(repr(keyboard_one))
|
||||
print(f"两个数据类对象按字段相等:{keyboard_one == keyboard_two}")
|
||||
print()
|
||||
|
||||
print("差异 3:__str__ 类似面向用户的 toString() 显示。")
|
||||
print(str(keyboard_one))
|
||||
print()
|
||||
|
||||
print("差异 4:类型注解不会自动校验,业务规则要在 __post_init__ 中实现。")
|
||||
try:
|
||||
Product("错误商品", -1.0)
|
||||
except ValueError as error:
|
||||
print(f"非法商品已被阻止:{error}")
|
||||
print()
|
||||
|
||||
print("差异 5:default_factory 确保不同购物车使用不同列表。")
|
||||
first_cart = Cart("小明")
|
||||
second_cart = Cart("小红")
|
||||
first_cart.add(keyboard_one)
|
||||
print(f"小明的商品数:{len(first_cart.items)}")
|
||||
print(f"小红的商品数:{len(second_cart.items)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user