feat(面向对象): 完成第三阶段差异化课程
This commit is contained in:
259
03_面向对象/3_1_Python与Java的类和对象/README.md
Normal file
259
03_面向对象/3_1_Python与Java的类和对象/README.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# 第 3-1 课:Python 与 Java 的类和对象
|
||||
|
||||
## 一、本课定位
|
||||
|
||||
你已经有成熟的 Java 开发经验,因此本阶段不再从“什么是对象”开始长篇讲解。本课采用差异驱动方式:先建立 Java 写法与 Python 写法的对应关系,再通过代码和练习形成 Python 语感。
|
||||
|
||||
即使没有完整阅读本文,也请至少运行示例并完成 `practice.py`。示例输出和练习题中会重复本课的关键差异。
|
||||
|
||||
## 二、本课目标
|
||||
|
||||
完成本课后,你将能够:
|
||||
|
||||
1. 使用 Python 定义类、初始化对象并调用实例方法;
|
||||
2. 理解 `self` 与 Java `this` 的关系和语法差异;
|
||||
3. 区分实例属性与类属性;
|
||||
4. 区分实例方法、类方法和静态方法;
|
||||
5. 避免把 Java 的字段声明、构造器和 `new` 原样搬到 Python;
|
||||
6. 理解 Python 对象模型更动态,但工程代码仍应保持清晰约束。
|
||||
|
||||
## 三、前置知识
|
||||
|
||||
- 已有 Java 类、对象、构造器、实例字段和 `static` 成员经验;
|
||||
- 已学习 Python 函数、字典和类型注解;
|
||||
- 能在 PowerShell 中运行 Python 文件。
|
||||
|
||||
## 四、先看差异速查表
|
||||
|
||||
| 关注点 | Java | Python |
|
||||
|---|---|---|
|
||||
| 定义类 | `public class Agent` | `class Agent:` |
|
||||
| 创建对象 | `new Agent(...)` | `Agent(...)`,没有 `new` |
|
||||
| 初始化入口 | 与类同名的构造器 | `__init__` 特殊方法 |
|
||||
| 当前对象 | 隐式 `this` | 定义方法时显式写 `self` |
|
||||
| 实例字段 | 通常在类体中声明 | 通常在 `__init__` 中赋值创建 |
|
||||
| 类级数据 | `static` 字段 | 类属性 |
|
||||
| 实例方法 | 默认接收隐式 `this` | 第一个参数通常写 `self` |
|
||||
| 类级工厂 | 常用 `static` 方法 | 常用 `@classmethod` 和 `cls` |
|
||||
| 静态工具方法 | `static` 方法 | `@staticmethod`,无 `self`/`cls` |
|
||||
| 访问控制 | `public/protected/private` | 主要依赖命名约定,后续课详讲 |
|
||||
| 方法重载 | 支持同名不同参数 | 不按 Java 方式支持,后续课详讲 |
|
||||
|
||||
## 五、最小类定义
|
||||
|
||||
```python
|
||||
class Agent:
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
def describe(self) -> str:
|
||||
return f"Agent:{self.name}"
|
||||
|
||||
|
||||
agent = Agent("代码助手")
|
||||
print(agent.describe())
|
||||
```
|
||||
|
||||
对应 Java 思维可以理解为:
|
||||
|
||||
- `__init__` 负责接收初始化数据,但它不是与类同名的构造器;
|
||||
- `self.name = name` 在当前实例上创建并赋值 `name` 属性;
|
||||
- `self` 类似 `this`,只是 Python 要在方法定义中明确写出来;
|
||||
- 调用 `agent.describe()` 时不传 `self`,解释器会自动传入 `agent`;
|
||||
- 创建对象直接调用 `Agent(...)`,不写 `new`。
|
||||
|
||||
严格来说,Python 先通过 `__new__` 创建对象,再由 `__init__` 初始化对象。本阶段日常业务代码先掌握 `__init__` 即可,暂不展开 `__new__`。
|
||||
|
||||
## 六、Python 属性不要求预先声明
|
||||
|
||||
Java 通常先声明字段:
|
||||
|
||||
```java
|
||||
private String name;
|
||||
```
|
||||
|
||||
Python 常在 `__init__` 中直接建立实例属性:
|
||||
|
||||
```python
|
||||
self.name = name
|
||||
```
|
||||
|
||||
Python 甚至允许对象创建后动态添加属性:
|
||||
|
||||
```python
|
||||
agent.status = "工作中"
|
||||
```
|
||||
|
||||
这体现了 Python 的动态性。它不是鼓励随意改变对象结构;在成熟项目中,仍应尽量在 `__init__` 中集中建立稳定的实例属性,让阅读者和类型检查工具更容易理解对象。
|
||||
|
||||
## 七、实例属性与类属性
|
||||
|
||||
```python
|
||||
class Agent:
|
||||
platform = "Codex"
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
```
|
||||
|
||||
- `self.name` 是实例属性,每个对象可以保存不同值;
|
||||
- `Agent.platform` 是类属性,由类统一提供,作用类似 Java `static` 字段;
|
||||
- `agent.platform` 也能读取类属性,但类级含义明确时优先使用 `Agent.platform`;
|
||||
- 如果执行 `agent.platform = "其他平台"`,通常会在该实例上创建同名实例属性,从而遮蔽类属性,而不是修改整个类的值。
|
||||
|
||||
最后一点与 Java 字段访问直觉不同,是常见问题。
|
||||
|
||||
## 八、三种方法
|
||||
|
||||
### 8.1 实例方法
|
||||
|
||||
```python
|
||||
def describe(self) -> str:
|
||||
return self.name
|
||||
```
|
||||
|
||||
通过 `self` 访问当前对象,适合处理实例状态。
|
||||
|
||||
### 8.2 类方法
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def from_config(cls, config: dict[str, str]) -> "Agent":
|
||||
return cls(config["name"])
|
||||
```
|
||||
|
||||
装饰器(Decorator)`@classmethod` 会让方法接收当前类 `cls`。它很适合命名工厂:使用 `cls(...)` 而非写死 `Agent(...)`,子类继承时更自然。
|
||||
|
||||
### 8.3 静态方法
|
||||
|
||||
```python
|
||||
@staticmethod
|
||||
def is_valid_name(name: str) -> bool:
|
||||
return bool(name.strip())
|
||||
```
|
||||
|
||||
静态方法不接收 `self` 或 `cls`。它只是逻辑上属于该类,但不需要访问实例状态或类状态。
|
||||
|
||||
不要为了模仿 Java 工具类而大量使用静态方法。普通模块函数也是 Python 中很自然的组织方式。
|
||||
|
||||
## 九、完整示例
|
||||
|
||||
示例文件:
|
||||
|
||||
```text
|
||||
03_面向对象/3_1_Python与Java的类和对象/class_object_comparison.py
|
||||
```
|
||||
|
||||
示例会直接输出五项差异:
|
||||
|
||||
1. `__init__` 与 Java 构造器的区别;
|
||||
2. `self` 与 `this` 的区别;
|
||||
3. Python 属性的动态创建;
|
||||
4. 类属性与 `static` 字段的对应关系;
|
||||
5. Python 创建对象不写 `new`。
|
||||
|
||||
## 十、运行方法
|
||||
|
||||
在项目根目录 `D:\Code\Python` 打开 PowerShell,运行:
|
||||
|
||||
```powershell
|
||||
python .\03_面向对象\3_1_Python与Java的类和对象\class_object_comparison.py
|
||||
```
|
||||
|
||||
完成练习后运行:
|
||||
|
||||
```powershell
|
||||
python .\03_面向对象\3_1_Python与Java的类和对象\practice.py
|
||||
```
|
||||
|
||||
## 十一、示例预期结果
|
||||
|
||||
输出中应依次看到“差异 1”到“差异 5”,并看到:
|
||||
|
||||
```text
|
||||
代码助手 使用 gpt-5.2,运行平台:Codex
|
||||
动态添加的 status:工作中
|
||||
通过类读取:Codex;通过实例读取:Codex
|
||||
审查助手 使用 gpt-5,运行平台:Codex
|
||||
名称是否合法:True
|
||||
```
|
||||
|
||||
## 十二、Java 开发者常见误区
|
||||
|
||||
### 12.1 创建对象时写 `new`
|
||||
|
||||
Python 没有这种语法,直接写:
|
||||
|
||||
```python
|
||||
agent = Agent("代码助手")
|
||||
```
|
||||
|
||||
### 12.2 调用方法时手动传 `self`
|
||||
|
||||
调用 `agent.describe()` 时 Python 会自动绑定实例,不要写 `agent.describe(agent)`。
|
||||
|
||||
### 12.3 忘记在方法定义中写 `self`
|
||||
|
||||
实例方法定义必须显式接收当前对象:
|
||||
|
||||
```python
|
||||
def describe(self) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
`self` 不是保留关键字,但它是整个 Python 社区遵守的标准命名,不要改成其他名字。
|
||||
|
||||
### 12.4 把所有工厂方法都写成静态方法
|
||||
|
||||
如果方法需要创建“当前类”的实例,优先考虑 `@classmethod` 和 `cls(...)`,这比写死类名更适合继承。
|
||||
|
||||
### 12.5 认为没有 `private` 就无法封装
|
||||
|
||||
Python 更依赖约定、属性和接口设计。单下划线、双下划线及 `property` 会在下一课结合 Java 访问控制集中讲解。
|
||||
|
||||
### 12.6 用多个同名方法实现重载
|
||||
|
||||
Python 类体中后定义的同名方法会覆盖前一个定义,不能照搬 Java 重载。常见替代方式包括默认参数、关键字参数和单分派,后续按需讲解。
|
||||
|
||||
## 十三、课堂练习
|
||||
|
||||
打开 `practice.py`,完成一个 `Book` 类。练习覆盖:
|
||||
|
||||
1. `__init__` 和实例属性;
|
||||
2. `self` 和实例方法;
|
||||
3. 类属性;
|
||||
4. `@classmethod` 命名工厂;
|
||||
5. `@staticmethod` 校验方法;
|
||||
6. 不使用 `new` 创建并运行对象。
|
||||
|
||||
每部分都附有 Java 对照提醒和最终预期输出,因此可以直接从练习开始。
|
||||
|
||||
## 十四、参考答案
|
||||
|
||||
参考答案暂不写入 `practice.py`,避免直接照抄。完成后可以把代码交给我,我会从以下三方面反馈:
|
||||
|
||||
1. 正确性:对象创建、属性和方法结果是否正确;
|
||||
2. 可读性:命名、结构和注释是否符合 Python 习惯;
|
||||
3. 知识掌握:是否仍残留 Java 式写法,能否解释选择原因。
|
||||
|
||||
## 十五、本课小结
|
||||
|
||||
- Python 用 `class` 定义类,但通常不写 `public`;
|
||||
- 使用 `__init__` 初始化实例,创建对象时不写 `new`;
|
||||
- `self` 类似 Java `this`,在定义实例方法时必须显式声明;
|
||||
- 实例属性通常通过 `self.xxx` 在 `__init__` 中建立;
|
||||
- 类属性类似 `static` 字段,但实例同名赋值会产生遮蔽;
|
||||
- `@classmethod` 接收 `cls`,适合可继承的命名工厂;
|
||||
- `@staticmethod` 不接收实例或类,只处理与类有关的独立逻辑;
|
||||
- Python 更动态,但成熟项目仍需要稳定、清晰的对象结构。
|
||||
|
||||
## 十六、验收标准
|
||||
|
||||
- 能不使用 `new` 创建 Python 对象;
|
||||
- 能正确编写 `__init__` 和 `self`;
|
||||
- 能解释 `self` 与 Java `this` 的语法差异;
|
||||
- 能区分实例属性和类属性;
|
||||
- 能区分实例方法、类方法和静态方法;
|
||||
- 能解释为什么命名工厂中优先使用 `cls(...)`;
|
||||
- 能指出 Python 不支持 Java 式同名方法重载;
|
||||
- `practice.py` 的实际输出与题目预期一致。
|
||||
63
03_面向对象/3_1_Python与Java的类和对象/class_object_comparison.py
Normal file
63
03_面向对象/3_1_Python与Java的类和对象/class_object_comparison.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# 第 3-1 课完整示例:Python 与 Java 的类和对象
|
||||
#
|
||||
# 本示例不重复讲解通用的面向对象概念,而是集中展示 Python 与 Java 的写法差异。
|
||||
# 运行程序时,每一段输出都会再次提醒对应差异,便于只看代码和结果也能掌握重点。
|
||||
|
||||
|
||||
class Agent:
|
||||
"""表示一个简单的 Agent,并演示 Python 类的核心语法。"""
|
||||
|
||||
# Python 的类属性类似 Java 的 static 字段,由所有实例共同访问。
|
||||
platform = "Codex"
|
||||
|
||||
def __init__(self, name: str, model: str = "gpt-5") -> None:
|
||||
"""初始化实例属性;__init__ 的作用接近 Java 构造器,但它不是类名同名方法。"""
|
||||
# Python 通常直接在 __init__ 中创建实例属性,不需要提前声明字段。
|
||||
# self 类似 Java 的 this,但在实例方法定义中必须显式写出来。
|
||||
self.name = name
|
||||
self.model = model
|
||||
|
||||
def describe(self) -> str:
|
||||
"""返回当前对象的说明文字。"""
|
||||
return f"{self.name} 使用 {self.model},运行平台:{self.platform}"
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict[str, str]) -> "Agent":
|
||||
"""通过配置创建对象;cls 指向当前类,作用类似可继承的命名工厂。"""
|
||||
return cls(config["name"], config.get("model", "gpt-5"))
|
||||
|
||||
@staticmethod
|
||||
def is_valid_name(name: str) -> bool:
|
||||
"""校验名称;该方法不需要访问某个对象或当前类。"""
|
||||
return bool(name.strip())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""创建对象并输出 Python 与 Java 的关键差异。"""
|
||||
code_agent = Agent("代码助手", "gpt-5.2")
|
||||
review_agent = Agent.from_config({"name": "审查助手"})
|
||||
|
||||
print("差异 1:Python 使用 __init__ 初始化对象,不写与类同名的构造器。")
|
||||
print(code_agent.describe())
|
||||
print()
|
||||
|
||||
print("差异 2:self 类似 Java 的 this,但定义实例方法时必须显式声明。")
|
||||
print(f"通过对象访问实例属性:{code_agent.name}")
|
||||
print()
|
||||
|
||||
print("差异 3:实例属性通常在运行时创建,不需要像 Java 字段那样预先声明。")
|
||||
code_agent.status = "工作中"
|
||||
print(f"动态添加的 status:{code_agent.status}")
|
||||
print()
|
||||
|
||||
print("差异 4:类属性类似 static 字段,但也能通过实例读取。")
|
||||
print(f"通过类读取:{Agent.platform};通过实例读取:{review_agent.platform}")
|
||||
print()
|
||||
|
||||
print("差异 5:Python 不使用 new,直接调用类即可创建对象。")
|
||||
print(review_agent.describe())
|
||||
print(f"名称是否合法:{Agent.is_valid_name(review_agent.name)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
82
03_面向对象/3_1_Python与Java的类和对象/practice.py
Normal file
82
03_面向对象/3_1_Python与Java的类和对象/practice.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# 第 3-1 课课堂练习:Python 与 Java 的类和对象
|
||||
#
|
||||
# 这份练习把关键差异直接写进题目,不要求先完整阅读讲义。
|
||||
# 请先独立完成;不要删除题目、预期输出和自查说明。
|
||||
|
||||
|
||||
# 第一部分:定义 Book 类
|
||||
# 1. 不要像 Java 一样预先声明 title、author、price 字段。
|
||||
# 2. 定义 __init__(self, title, author, price=0.0) 方法。
|
||||
# 3. 为 title 和 author 添加 str 类型注解,为 price 添加 float 类型注解。
|
||||
# 4. 返回值标注为 None。
|
||||
# 5. 在 __init__ 中通过 self.title、self.author、self.price 创建实例属性。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - Python 的 __init__ 承担初始化职责,但不是“与类同名的构造器”;
|
||||
# - self 类似 Java 的 this,但必须写在方法参数列表中;
|
||||
# - 创建对象时不写 new,也不手动传入 self。
|
||||
|
||||
|
||||
# 第二部分:定义实例方法 get_summary(self)
|
||||
# 1. 返回值标注为 str。
|
||||
# 2. 返回格式:书名:《Python 后端开发》|作者:小明|价格:59.0 元。
|
||||
# 3. 必须通过 self 读取三个实例属性。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - Python 方法没有 public 关键字;
|
||||
# - 调用时写 book.get_summary(),Python 会自动把 book 传给 self。
|
||||
|
||||
# 第三部分:添加类属性 category
|
||||
# 1. 在 Book 类中、所有方法外定义 category = "编程"。
|
||||
# 2. 在后面的 main() 中分别使用 Book.category 和 book.category 输出它。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - 类属性在用途上类似 Java 的 static 字段;
|
||||
# - 为避免实例属性遮蔽类属性,本题不要执行 book.category = ...。
|
||||
|
||||
|
||||
# 第四部分:定义类方法 from_text(cls, text)
|
||||
# 1. 在方法上方添加 @classmethod。
|
||||
# 2. text 的格式固定为“书名|作者”,例如“流畅的 Python|Luciano”。
|
||||
# 3. 使用 text.split("|") 得到书名和作者。
|
||||
# 4. 使用 cls(title, author) 创建并返回对象,让 price 使用默认值。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - cls 指向当前类,classmethod 常用于实现命名工厂;
|
||||
# - 不要把类名 Book 写死在 return 中,使用 cls 更利于继承。
|
||||
|
||||
|
||||
# 第五部分:定义静态方法 is_valid_price(price)
|
||||
# 1. 在方法上方添加 @staticmethod。
|
||||
# 2. price 标注为 float,返回值标注为 bool。
|
||||
# 3. price 大于或等于 0 时返回 True,否则返回 False。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - staticmethod 不接收 self 或 cls;
|
||||
# - 当逻辑与 Book 有关、但不需要对象状态和类状态时,可以使用它。
|
||||
|
||||
|
||||
# 第六部分:定义 main() 并完成运行验证
|
||||
# 1. 使用 Book("Python 后端开发", "小明", 59.0) 创建 book,注意不写 new。
|
||||
# 2. 输出 book.get_summary()。
|
||||
# 3. 分别通过类和对象输出 category。
|
||||
# 4. 使用 Book.from_text("流畅的 Python|Luciano") 创建 imported_book。
|
||||
# 5. 输出 imported_book.get_summary(),其价格应为默认值 0.0。
|
||||
# 6. 输出 Book.is_valid_price(59.0) 和 Book.is_valid_price(-1.0)。
|
||||
# 7. 添加程序入口判断,直接运行本文件时调用 main()。
|
||||
#
|
||||
# 预期输出:
|
||||
# 书名:《Python 后端开发》|作者:小明|价格:59.0 元
|
||||
# 通过类读取分类:编程
|
||||
# 通过对象读取分类:编程
|
||||
# 书名:《流畅的 Python》|作者:Luciano|价格:0.0 元
|
||||
# 59.0 是合法价格:True
|
||||
# -1.0 是合法价格:False
|
||||
|
||||
|
||||
# 完成后自查:
|
||||
# 1. 是否能解释 self 与 Java this 的相同点和写法差异;
|
||||
# 2. 是否记住 Python 创建对象时不写 new;
|
||||
# 3. 是否知道 __init__ 负责初始化,而不是 Java 式的同名构造器;
|
||||
# 4. 是否能区分实例属性、类属性、实例方法、类方法和静态方法;
|
||||
# 5. 是否没有在类外直接替学习者写出答案。
|
||||
244
03_面向对象/3_2_Python与Java的封装差异/README.md
Normal file
244
03_面向对象/3_2_Python与Java的封装差异/README.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# 第 3-2 课:Python 与 Java 的封装差异
|
||||
|
||||
## 一、本课定位
|
||||
|
||||
Java 通过 `public`、`protected`、`private` 等访问修饰符实施较严格的访问控制。Python 更信任开发者,主要使用命名约定和属性(Property)表达接口边界。
|
||||
|
||||
本课不重复封装的一般理论,只讲 Java 开发者迁移到 Python 时必须调整的认识。
|
||||
|
||||
## 二、本课目标
|
||||
|
||||
完成本课后,你将能够:
|
||||
|
||||
1. 理解 Python 没有与 Java 完全等价的强制 `private`;
|
||||
2. 解释普通名称、单下划线和双下划线的区别;
|
||||
3. 使用 `@property` 提供读取接口;
|
||||
4. 使用 `@属性名.setter` 在赋值时执行校验;
|
||||
5. 判断何时直接使用公开属性,何时需要 property;
|
||||
6. 避免把 Java getter/setter 机械翻译成 Python 方法。
|
||||
|
||||
## 三、前置知识
|
||||
|
||||
- Java 的访问修饰符、getter 和 setter;
|
||||
- Python 类、`self`、实例属性和实例方法;
|
||||
- Python 异常处理。
|
||||
|
||||
## 四、差异速查表
|
||||
|
||||
| 意图 | Java 常见写法 | Python 常见写法 |
|
||||
|---|---|---|
|
||||
| 公开成员 | `public` | 普通名称,例如 `name` |
|
||||
| 仅供内部使用 | `protected` 或包访问 | `_name` 命名约定 |
|
||||
| 避免子类意外覆盖 | `private` | `__name` 触发名称改写 |
|
||||
| 读取属性 | `getPrice()` | `product.price` 或 `@property` |
|
||||
| 校验后赋值 | `setPrice(value)` | `product.price = value` 配合 setter |
|
||||
| 强制安全边界 | 访问修饰符 | 下划线不能提供真正的安全隔离 |
|
||||
|
||||
## 五、单下划线:约定,不是限制
|
||||
|
||||
```python
|
||||
self._owner = owner
|
||||
```
|
||||
|
||||
`_owner` 表示“这是内部实现,外部代码通常不要直接依赖”。但是下面的代码仍然可以运行:
|
||||
|
||||
```python
|
||||
print(account._owner)
|
||||
```
|
||||
|
||||
因此,单下划线主要是团队沟通约定,不是运行时访问控制。
|
||||
|
||||
## 六、双下划线:名称改写,不是绝对私有
|
||||
|
||||
```python
|
||||
self.__balance = 100.0
|
||||
```
|
||||
|
||||
Python 会进行名称改写(Name Mangling),把属性名称处理成类似:
|
||||
|
||||
```text
|
||||
_Account__balance
|
||||
```
|
||||
|
||||
主要目的之一是避免子类无意中使用相同名称覆盖父类属性。外部仍能通过改写后的名字访问它,所以它不是安全机制,也不能保护密码或密钥。
|
||||
|
||||
业务项目通常不要把所有内部属性都写成双下划线。只有确实需要避免子类名称冲突时再使用。
|
||||
|
||||
## 七、property:保持属性语法,同时执行方法逻辑
|
||||
|
||||
Java 常见写法:
|
||||
|
||||
```java
|
||||
public double getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
public void setBalance(double value) {
|
||||
if (value < 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
balance = value;
|
||||
}
|
||||
```
|
||||
|
||||
Python 可以写成:
|
||||
|
||||
```python
|
||||
@property
|
||||
def balance(self) -> float:
|
||||
return self.__balance
|
||||
|
||||
@balance.setter
|
||||
def balance(self, value: float) -> None:
|
||||
if value < 0:
|
||||
raise ValueError("余额不能小于 0。")
|
||||
self.__balance = value
|
||||
```
|
||||
|
||||
调用方式仍像普通属性:
|
||||
|
||||
```python
|
||||
print(account.balance)
|
||||
account.balance = 200.0
|
||||
```
|
||||
|
||||
读取时实际执行 getter 方法,赋值时实际执行 setter 方法。
|
||||
|
||||
## 八、不要为每个字段机械创建 property
|
||||
|
||||
如果属性只是公开保存数据,没有校验、计算或兼容需求,可以直接使用:
|
||||
|
||||
```python
|
||||
self.name = name
|
||||
```
|
||||
|
||||
不需要照搬 Java,为它创建无逻辑的 `get_name()` 和 `set_name()`。
|
||||
|
||||
适合使用 property 的情况包括:
|
||||
|
||||
- 赋值时需要校验;
|
||||
- 返回值需要动态计算;
|
||||
- 希望提供只读接口;
|
||||
- 内部存储方式可能改变,但不希望调用方式变化。
|
||||
|
||||
## 九、只读 property
|
||||
|
||||
只定义 getter、不定义 setter:
|
||||
|
||||
```python
|
||||
@property
|
||||
def owner(self) -> str:
|
||||
return self._owner
|
||||
```
|
||||
|
||||
调用者可以读取 `account.owner`,但执行 `account.owner = "其他人"` 时会报错。
|
||||
|
||||
需要注意:这仍然不是不可绕过的安全边界。调用者如果直接修改 `_owner`,Python 不会阻止。它表达的是正常接口,不是对恶意代码的防护。
|
||||
|
||||
## 十、初始化时复用 setter
|
||||
|
||||
下面的写法可以让初始值和后续赋值使用相同校验:
|
||||
|
||||
```python
|
||||
def __init__(self, balance: float) -> None:
|
||||
self.__balance = 0.0
|
||||
self.balance = balance
|
||||
```
|
||||
|
||||
第二行 `self.balance = balance` 会调用 property setter。这样不会出现“对象创建时允许负数,创建后却不允许负数”的规则不一致。
|
||||
|
||||
## 十一、完整示例
|
||||
|
||||
示例文件:
|
||||
|
||||
```text
|
||||
03_面向对象/3_2_Python与Java的封装差异/encapsulation_comparison.py
|
||||
```
|
||||
|
||||
运行命令:
|
||||
|
||||
```powershell
|
||||
python .\03_面向对象\3_2_Python与Java的封装差异\encapsulation_comparison.py
|
||||
```
|
||||
|
||||
示例输出会直接展示:
|
||||
|
||||
1. 单下划线属性仍能从外部访问;
|
||||
2. property 使用属性语法执行校验逻辑;
|
||||
3. 双下划线在对象中实际发生了名称改写;
|
||||
4. 非法余额会被 setter 阻止。
|
||||
|
||||
## 十二、常见误区
|
||||
|
||||
### 12.1 认为 `_name` 等于 Java protected
|
||||
|
||||
它只是一种命名约定,不会限制外部或非子类代码访问。
|
||||
|
||||
### 12.2 认为 `__name` 能保护敏感信息
|
||||
|
||||
双下划线不是加密或权限控制。敏感信息仍需使用安全的配置、存储和权限机制。
|
||||
|
||||
### 12.3 为所有属性生成 getter 和 setter
|
||||
|
||||
没有额外逻辑时,直接访问公开属性更符合 Python 风格。需要保持属性语法并增加逻辑时再使用 property。
|
||||
|
||||
### 12.4 在 setter 内给 property 本身赋值
|
||||
|
||||
下面会无限递归:
|
||||
|
||||
```python
|
||||
@balance.setter
|
||||
def balance(self, value: float) -> None:
|
||||
self.balance = value
|
||||
```
|
||||
|
||||
setter 内应给实际存储属性赋值,例如 `self.__balance = value`。
|
||||
|
||||
## 十三、课堂练习
|
||||
|
||||
打开:
|
||||
|
||||
```text
|
||||
03_面向对象/3_2_Python与Java的封装差异/practice.py
|
||||
```
|
||||
|
||||
完成 `Product` 类,练习内容包括:
|
||||
|
||||
1. 单下划线和双下划线属性;
|
||||
2. 只读 `name` property;
|
||||
3. 可读写 `price` property;
|
||||
4. 价格与折扣率校验;
|
||||
5. 使用 `try...except` 验证非法数据。
|
||||
|
||||
练习题中包含 Java 对照提醒和完整预期输出,可以直接从练习开始。
|
||||
|
||||
## 十四、参考答案
|
||||
|
||||
参考答案暂不写入练习文件。完成后,我会按照以下层级验证:
|
||||
|
||||
- 必须修复:语法错误、运行错误、业务结果错误、关键知识点使用错误;
|
||||
- 建议改进:重复逻辑、容易产生误解的结构、明显影响可读性的写法;
|
||||
- 可选优化:非必要类型注解、输出美化和不影响理解的风格统一。
|
||||
|
||||
只有“必须修复”的问题会阻止课程验收。
|
||||
|
||||
## 十五、本课小结
|
||||
|
||||
- Python 主要通过约定和接口设计实现封装;
|
||||
- `_name` 表示内部使用,但外部仍能访问;
|
||||
- `__name` 触发名称改写,主要用于避免子类名称冲突;
|
||||
- 双下划线不是 Java `private` 的完全等价物,也不是安全机制;
|
||||
- `@property` 让方法逻辑保留属性式调用体验;
|
||||
- setter 可以集中完成赋值校验;
|
||||
- 没有额外逻辑时,不必机械创建 getter 和 setter。
|
||||
|
||||
## 十六、验收标准
|
||||
|
||||
- 能解释单下划线和双下划线的区别;
|
||||
- 能说明下划线为什么不能保护敏感信息;
|
||||
- 能定义只读 property;
|
||||
- 能定义包含校验的 property setter;
|
||||
- 能避免 setter 自我赋值造成无限递归;
|
||||
- 能用 property 统一初始化和后续赋值规则;
|
||||
- `practice.py` 能正确阻止负数价格;
|
||||
- 能说明 Python property 与 Java getter/setter 的调用差异。
|
||||
69
03_面向对象/3_2_Python与Java的封装差异/encapsulation_comparison.py
Normal file
69
03_面向对象/3_2_Python与Java的封装差异/encapsulation_comparison.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# 第 3-2 课完整示例:Python 与 Java 的封装差异
|
||||
#
|
||||
# 本示例重点演示 Python 的下划线命名约定和 @property。
|
||||
# 运行时会直接输出关键结论,不完整阅读讲义也能看到本课重点。
|
||||
|
||||
|
||||
class Account:
|
||||
"""表示一个账户,并演示 Python 常见的封装方式。"""
|
||||
|
||||
def __init__(self, owner: str, balance: float = 0.0) -> None:
|
||||
# 单下划线表示“仅供内部使用”的约定,但不会禁止外部访问。
|
||||
self._owner = owner
|
||||
|
||||
# 双下划线会触发名称改写(Name Mangling),避免子类意外覆盖同名属性。
|
||||
# 它不是 Java private 那样的安全边界,也不能用于保护敏感信息。
|
||||
self.__balance = 0.0
|
||||
self.balance = balance
|
||||
|
||||
@property
|
||||
def owner(self) -> str:
|
||||
"""允许调用者以 account.owner 的形式读取账户所有者。"""
|
||||
return self._owner
|
||||
|
||||
@property
|
||||
def balance(self) -> float:
|
||||
"""读取余额;调用时不需要写成 get_balance()。"""
|
||||
return self.__balance
|
||||
|
||||
@balance.setter
|
||||
def balance(self, value: float) -> None:
|
||||
"""设置余额,并集中执行非负校验。"""
|
||||
if value < 0:
|
||||
raise ValueError("余额不能小于 0。")
|
||||
self.__balance = value
|
||||
|
||||
def deposit(self, amount: float) -> None:
|
||||
"""存入资金,并复用 balance 属性中的校验入口。"""
|
||||
if amount <= 0:
|
||||
raise ValueError("存入金额必须大于 0。")
|
||||
self.balance = self.balance + amount
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""运行封装差异示例。"""
|
||||
account = Account("小明", 100.0)
|
||||
|
||||
print("差异 1:Python 主要依靠命名约定表达成员用途。")
|
||||
print(f"单下划线属性仍可访问:{account._owner}")
|
||||
print()
|
||||
|
||||
print("差异 2:@property 让方法校验和普通属性访问可以同时存在。")
|
||||
print(f"{account.owner} 当前余额:{account.balance}")
|
||||
account.balance = 150.0
|
||||
print(f"赋值后的余额:{account.balance}")
|
||||
print()
|
||||
|
||||
print("差异 3:双下划线是名称改写,不是绝对私有或安全机制。")
|
||||
print(f"对象中保存的实际属性名:{account.__dict__}")
|
||||
print()
|
||||
|
||||
print("差异 4:属性赋值可以统一执行校验。")
|
||||
try:
|
||||
account.balance = -1.0
|
||||
except ValueError as error:
|
||||
print(f"非法赋值已被阻止:{error}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
70
03_面向对象/3_2_Python与Java的封装差异/practice.py
Normal file
70
03_面向对象/3_2_Python与Java的封装差异/practice.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# 第 3-2 课课堂练习:Python 与 Java 的封装差异
|
||||
#
|
||||
# 关键规则已经写进每部分题目,可以不先完整阅读讲义。
|
||||
# 请保留题目、预期结果和自查说明,并独立完成 Product 类。
|
||||
|
||||
|
||||
# 第一部分:定义 Product 类和初始化方法
|
||||
# 1. 定义 __init__(self, name, price) 方法。
|
||||
# 2. name 标注为 str,price 标注为 float,返回值标注可自行决定是否添加。
|
||||
# 3. 使用 self._name = name 保存名称。
|
||||
# 4. 使用 self.__price = 0.0 建立双下划线属性。
|
||||
# 5. 最后执行 self.price = price,通过后面定义的 setter 完成初始价格校验。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - _name 只表示“内部使用”的约定,外部访问不会产生语法错误;
|
||||
# - __price 会触发名称改写,但不等于 Java 的强制 private;
|
||||
# - 双下划线不能作为保存密码等敏感信息的安全措施。
|
||||
|
||||
# 第二部分:为 name 定义只读 property
|
||||
# 1. 在方法上方添加 @property。
|
||||
# 2. 定义 name(self) 方法并返回 self._name。
|
||||
# 3. 不要定义 @name.setter,使调用者不能通过 product.name = ... 正常修改名称。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - Java 常写 getName();Python 调用时直接写 product.name;
|
||||
# - 虽然调用形式像字段,但实际上会执行 name() 方法。
|
||||
|
||||
|
||||
# 第三部分:为 price 定义可读写 property
|
||||
# 1. 使用 @property 定义 price(self),返回 self.__price。
|
||||
# 2. 使用 @price.setter 定义同名方法 price(self, value)。
|
||||
# 3. value 标注为 float。
|
||||
# 4. value 小于 0 时执行 raise ValueError("价格不能小于 0。")。
|
||||
# 5. 校验通过后执行 self.__price = value。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - 这相当于 getPrice() 和 setPrice(),但调用形式仍是 product.price;
|
||||
# - 不要同时保留公开的 price 实例属性,否则会和 property 的职责混淆。
|
||||
|
||||
|
||||
# 第四部分:定义 discount(self, rate) 实例方法
|
||||
# 1. rate 标注为 float。
|
||||
# 2. rate 必须满足 0 <= rate <= 1,否则抛出 ValueError("折扣率必须在 0 到 1 之间。")。
|
||||
# 3. 合法时执行 self.price = self.price * rate。
|
||||
# 4. 通过 property 修改价格,复用统一的价格入口。
|
||||
|
||||
|
||||
# 第五部分:定义 main() 并验证行为
|
||||
# 1. 创建 Product("机械键盘", 500.0)。
|
||||
# 2. 输出“商品:机械键盘|价格:500.0 元”。
|
||||
# 3. 执行 product.price = 450.0,再输出“修改后价格:450.0 元”。
|
||||
# 4. 执行 product.discount(0.8),再输出“折扣后价格:360.0 元”。
|
||||
# 5. 使用 try...except 测试 product.price = -1.0。
|
||||
# 6. 捕获 ValueError 后输出“非法价格已被阻止:价格不能小于 0。”。
|
||||
# 7. 添加程序入口判断并调用 main()。
|
||||
#
|
||||
# 预期输出:
|
||||
# 商品:机械键盘|价格:500.0 元
|
||||
# 修改后价格:450.0 元
|
||||
# 折扣后价格:360.0 元
|
||||
# 非法价格已被阻止:价格不能小于 0。
|
||||
|
||||
|
||||
# 完成后自查:
|
||||
# 1. 是否知道单下划线只是内部使用约定;
|
||||
# 2. 是否知道双下划线会名称改写,但不是绝对私有;
|
||||
# 3. 是否能用 @property 代替简单的 Java getter;
|
||||
# 4. 是否能用 @属性名.setter 在赋值时执行校验;
|
||||
# 5. 是否能说明 product.price 看似访问字段、实际会执行方法;
|
||||
# 6. 是否只把运行错误和逻辑错误视为阻塞项,不把可选类型注解当作必须项。
|
||||
242
03_面向对象/3_3_Python与Java的继承和多态差异/README.md
Normal file
242
03_面向对象/3_3_Python与Java的继承和多态差异/README.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# 第 3-3 课:Python 与 Java 的继承和多态差异
|
||||
|
||||
## 一、本课定位
|
||||
|
||||
你已经掌握 Java 的继承、接口和多态,因此本课不重复通用理论,重点建立以下 Python 差异:
|
||||
|
||||
- `super()` 不需要显式写父类名;
|
||||
- Python 没有强制的 `@Override`;
|
||||
- Python 支持多继承,并使用方法解析顺序决定查找路径;
|
||||
- 多态调用经常基于鸭子类型,不要求对象继承共同父类或显式实现接口。
|
||||
|
||||
## 二、本课目标
|
||||
|
||||
完成本课后,你将能够:
|
||||
|
||||
1. 定义父类和子类并重写方法;
|
||||
2. 使用 `super()` 复用继承链中的实现;
|
||||
3. 解释方法解析顺序(Method Resolution Order,MRO);
|
||||
4. 使用鸭子类型(Duck Typing)完成统一调用;
|
||||
5. 理解 Python 多继承和 Java 单类继承的差异;
|
||||
6. 避免通过大量 `isinstance()` 分支模拟多态。
|
||||
|
||||
## 三、差异速查表
|
||||
|
||||
| 关注点 | Java | Python |
|
||||
|---|---|---|
|
||||
| 继承类 | `extends` | `class Child(Parent):` |
|
||||
| 调用继承逻辑 | `super(...)` | 常用 `super()` |
|
||||
| 重写标记 | 推荐 `@Override` | 没有强制对应标记 |
|
||||
| 类继承数量 | 单继承 | 支持多继承 |
|
||||
| 方法查找 | 类继承链 | 按 MRO 查找 |
|
||||
| 接口多态 | 通常显式 `implements` | 常见鸭子类型,无需声明接口 |
|
||||
| 类型判断 | 编译期和运行时类型体系 | 运行时更关注对象是否提供所需行为 |
|
||||
|
||||
## 四、继承和方法重写
|
||||
|
||||
```python
|
||||
class Notifier:
|
||||
def send(self, message: str) -> None:
|
||||
raise NotImplementedError("子类必须实现 send() 方法。")
|
||||
|
||||
|
||||
class EmailNotifier(Notifier):
|
||||
def send(self, message: str) -> None:
|
||||
print(f"发送邮件:{message}")
|
||||
```
|
||||
|
||||
Python 子类把父类写在类名后的圆括号中,不使用 `extends`。子类定义同名方法即可重写,语言本身不要求添加 `@Override`。
|
||||
|
||||
缺少 `@Override` 也意味着方法名拼错时不会获得同样的编译期保护,因此测试和静态检查更加重要。
|
||||
|
||||
## 五、正确理解 super()
|
||||
|
||||
```python
|
||||
class EmailNotifier(Notifier):
|
||||
def __init__(self, sender: str, address: str) -> None:
|
||||
super().__init__(sender)
|
||||
self.address = address
|
||||
```
|
||||
|
||||
Python 3 通常直接写 `super()`,不需要传入当前类和 `self`。
|
||||
|
||||
在简单单继承中,可以暂时把它理解为调用父类实现。但更准确的理解是:`super()` 会沿当前类的方法解析顺序继续查找方法。这个区别在多继承中尤其重要。
|
||||
|
||||
不要写死父类名称:
|
||||
|
||||
```python
|
||||
Notifier.__init__(self, sender)
|
||||
```
|
||||
|
||||
写死类名可能破坏多继承中的协作调用链。
|
||||
|
||||
## 六、方法解析顺序 MRO
|
||||
|
||||
Python 支持一个类继承多个父类:
|
||||
|
||||
```python
|
||||
class LoggedEmailNotifier(EmailNotifier, LogMixin):
|
||||
pass
|
||||
```
|
||||
|
||||
可以查看查找顺序:
|
||||
|
||||
```python
|
||||
print(LoggedEmailNotifier.mro())
|
||||
```
|
||||
|
||||
当调用某个方法时,Python 会按 MRO 中的顺序寻找第一个匹配实现。日常代码应避免设计过于复杂的多继承层次。
|
||||
|
||||
常见的安全用途是混入类(Mixin):它只提供一项小而明确的可复用能力,通常不独立代表完整业务对象。
|
||||
|
||||
## 七、鸭子类型
|
||||
|
||||
经典描述是:“如果一个对象走起来像鸭子、叫起来像鸭子,就把它当作鸭子使用。”
|
||||
|
||||
在代码中,这表示调用者更关心对象是否提供所需行为:
|
||||
|
||||
```python
|
||||
def send_message(notifier, message: str) -> None:
|
||||
notifier.send(message)
|
||||
```
|
||||
|
||||
下面两个类不必拥有共同父类:
|
||||
|
||||
```python
|
||||
class EmailNotifier:
|
||||
def send(self, message: str) -> None:
|
||||
...
|
||||
|
||||
|
||||
class ConsoleNotifier:
|
||||
def send(self, message: str) -> None:
|
||||
...
|
||||
```
|
||||
|
||||
只要对象具有可调用的 `send()` 方法,`send_message()` 就可以使用它。
|
||||
|
||||
与 Java 接口多态相比:
|
||||
|
||||
- Java 常在编译期要求对象声明实现某个接口;
|
||||
- Python 运行时可以直接尝试调用所需方法;
|
||||
- Python 也能使用协议(Protocol)为鸭子类型提供静态检查,后续结合类型设计再展开。
|
||||
|
||||
## 八、不要用类型分支代替多态
|
||||
|
||||
不推荐:
|
||||
|
||||
```python
|
||||
if isinstance(processor, CardProcessor):
|
||||
...
|
||||
elif isinstance(processor, QrCodeProcessor):
|
||||
...
|
||||
```
|
||||
|
||||
推荐让每个对象自己实现统一行为:
|
||||
|
||||
```python
|
||||
processor.pay(amount)
|
||||
```
|
||||
|
||||
这样新增处理器时,调用方通常不需要修改。
|
||||
|
||||
`isinstance()` 本身并非错误;当业务规则确实依赖类型时可以使用。但如果只是为了选择同一操作的不同实现,通常应优先使用多态。
|
||||
|
||||
## 九、继承还是组合
|
||||
|
||||
Java 与 Python 都不应为了复用少量代码而滥用继承。
|
||||
|
||||
- 真正存在“是一个”关系,并需要替换父类型行为时,可以考虑继承;
|
||||
- 只是“拥有一个”协作者或需要灵活替换能力时,优先考虑组合;
|
||||
- 只需要调用统一行为时,Python 的鸭子类型可能已经足够。
|
||||
|
||||
组合会在后续业务模型课程中结合实例继续练习。
|
||||
|
||||
## 十、完整示例和运行方法
|
||||
|
||||
示例文件:
|
||||
|
||||
```text
|
||||
03_面向对象/3_3_Python与Java的继承和多态差异/inheritance_polymorphism_comparison.py
|
||||
```
|
||||
|
||||
在项目根目录运行:
|
||||
|
||||
```powershell
|
||||
python .\03_面向对象\3_3_Python与Java的继承和多态差异\inheritance_polymorphism_comparison.py
|
||||
```
|
||||
|
||||
示例输出会直接展示:
|
||||
|
||||
1. `super()` 复用继承逻辑;
|
||||
2. 方法重写;
|
||||
3. 没有共同父类的鸭子类型调用;
|
||||
4. 多继承类的 MRO。
|
||||
|
||||
## 十一、常见错误
|
||||
|
||||
### 11.1 把 super() 当成固定父类对象
|
||||
|
||||
它实际按照 MRO 继续查找。在多继承中,下一站不一定是你凭直觉认定的某个父类。
|
||||
|
||||
### 11.2 忘记调用父类初始化逻辑
|
||||
|
||||
如果父类负责建立必要属性,子类通常需要调用 `super().__init__(...)`,否则使用父类属性时可能出现 `AttributeError`。
|
||||
|
||||
### 11.3 认为多态必须继承共同父类
|
||||
|
||||
Python 鸭子类型只要求对象提供当前操作需要的方法。
|
||||
|
||||
### 11.4 滥用多继承
|
||||
|
||||
多继承会增加 MRO 和初始化协作的理解成本。优先使用清晰的小型 Mixin,复杂业务能力通常更适合组合。
|
||||
|
||||
### 11.5 假设方法已经被正确重写
|
||||
|
||||
Python 没有强制 `@Override`。方法名或参数写错时,应通过测试、类型检查或代码审查发现。
|
||||
|
||||
## 十二、课堂练习
|
||||
|
||||
打开:
|
||||
|
||||
```text
|
||||
03_面向对象/3_3_Python与Java的继承和多态差异/practice.py
|
||||
```
|
||||
|
||||
完成支付处理器练习:
|
||||
|
||||
1. 定义父类统一操作;
|
||||
2. 子类通过 `super()` 复用初始化;
|
||||
3. 未继承父类的二维码处理器参与统一调用;
|
||||
4. 使用一个函数调用两种处理器;
|
||||
5. 输出并观察 `CardProcessor.mro()`。
|
||||
|
||||
## 十三、参考答案与验证标准
|
||||
|
||||
参考答案暂不写入练习文件。完成后按以下层级验证:
|
||||
|
||||
- 必须修复:语法错误、运行错误、支付结果错误、没有使用 `super()`、通过具体类型分支实现调用;
|
||||
- 建议改进:重复代码、职责混乱、明显影响理解的命名;
|
||||
- 可选优化:非必要类型注解、输出美化和不影响行为的格式问题。
|
||||
|
||||
只有“必须修复”会阻止课程验收。
|
||||
|
||||
## 十四、本课小结
|
||||
|
||||
- Python 使用 `class Child(Parent)` 表示继承;
|
||||
- 子类定义同名方法即可重写,没有强制 `@Override`;
|
||||
- `super()` 会沿 MRO 继续查找实现;
|
||||
- Python 支持多继承,但复杂多继承应谨慎使用;
|
||||
- 鸭子类型关注对象提供的行为,不要求共同父类或显式接口;
|
||||
- 统一调用不应依赖大量具体类型判断;
|
||||
- 继承用于真正的类型关系,单纯复用通常优先考虑组合。
|
||||
|
||||
## 十五、验收标准
|
||||
|
||||
- 能正确使用 `super().__init__()`;
|
||||
- 能重写父类方法;
|
||||
- 能输出并解释简单类的 MRO;
|
||||
- 能让未继承共同父类的对象参与统一调用;
|
||||
- 能解释鸭子类型与 Java 接口多态的区别;
|
||||
- 能避免通过 `isinstance()` 分支实现本题多态;
|
||||
- `practice.py` 输出正确支付结果。
|
||||
@@ -0,0 +1,78 @@
|
||||
# 第 3-3 课完整示例:Python 与 Java 的继承和多态差异
|
||||
#
|
||||
# 示例集中演示 super()、方法重写、多继承的方法解析顺序和鸭子类型。
|
||||
|
||||
|
||||
class Notifier:
|
||||
"""所有通知器都可以复用的基础类。"""
|
||||
|
||||
def __init__(self, sender: str) -> None:
|
||||
self.sender = sender
|
||||
|
||||
def send(self, message: str) -> None:
|
||||
"""定义统一操作;具体子类负责实现发送行为。"""
|
||||
raise NotImplementedError("子类必须实现 send() 方法。")
|
||||
|
||||
|
||||
class EmailNotifier(Notifier):
|
||||
"""通过继承实现邮件通知器。"""
|
||||
|
||||
def __init__(self, sender: str, address: str) -> None:
|
||||
# Python 3 中通常直接写 super(),不需要传入类名和 self。
|
||||
super().__init__(sender)
|
||||
self.address = address
|
||||
|
||||
def send(self, message: str) -> None:
|
||||
"""重写父类方法;Python 不要求添加 @Override。"""
|
||||
print(f"邮件|{self.sender} -> {self.address}:{message}")
|
||||
|
||||
|
||||
class ConsoleNotifier:
|
||||
"""没有继承 Notifier,但同样提供 send() 方法。"""
|
||||
|
||||
def send(self, message: str) -> None:
|
||||
print(f"控制台|{message}")
|
||||
|
||||
|
||||
class LogMixin:
|
||||
"""混入类(Mixin)只提供一项可复用能力。"""
|
||||
|
||||
def record(self) -> None:
|
||||
print("通知行为已记录。")
|
||||
|
||||
|
||||
class LoggedEmailNotifier(EmailNotifier, LogMixin):
|
||||
"""Python 可以继承多个类,查找方法时遵循 MRO。"""
|
||||
|
||||
|
||||
def send_message(notifier, message: str) -> None:
|
||||
"""只要传入对象具有 send() 方法,就可以完成调用。"""
|
||||
notifier.send(message)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""运行继承和多态差异示例。"""
|
||||
email = EmailNotifier("系统", "user@example.com")
|
||||
console = ConsoleNotifier()
|
||||
|
||||
print("差异 1:子类使用 super() 复用父类逻辑,不需要写父类名称。")
|
||||
print(f"邮件发送者:{email.sender}")
|
||||
print()
|
||||
|
||||
print("差异 2:Python 重写方法时没有强制的 @Override。")
|
||||
email.send("课程开始")
|
||||
print()
|
||||
|
||||
print("差异 3:鸭子类型关注对象有什么行为,而不是声明了什么接口。")
|
||||
send_message(email, "继承得到 send()")
|
||||
send_message(console, "未继承也能调用 send()")
|
||||
print()
|
||||
|
||||
print("差异 4:Python 支持多继承,并通过 MRO 决定方法查找顺序。")
|
||||
logged_email = LoggedEmailNotifier("系统", "admin@example.com")
|
||||
logged_email.record()
|
||||
print([class_type.__name__ for class_type in LoggedEmailNotifier.mro()])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
72
03_面向对象/3_3_Python与Java的继承和多态差异/practice.py
Normal file
72
03_面向对象/3_3_Python与Java的继承和多态差异/practice.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# 第 3-3 课课堂练习:Python 与 Java 的继承和多态差异
|
||||
#
|
||||
# 关键差异已经写进每部分题目,可以直接完成练习。
|
||||
# 请保留题目、预期结果和自查说明。
|
||||
|
||||
|
||||
# 第一部分:定义 PaymentProcessor 父类
|
||||
# 1. 定义 __init__(self, channel_name) 方法。
|
||||
# 2. channel_name 标注为 str,并保存为 self.channel_name。
|
||||
# 3. 定义 pay(self, amount) 方法,amount 标注为 float。
|
||||
# 4. pay() 中执行 raise NotImplementedError("子类必须实现 pay() 方法。")。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - 本题暂不使用抽象基类(Abstract Base Class,ABC),先观察普通父类的写法;
|
||||
# - Python 重写方法时不要求 @Override,拼错方法名也不会由该注解提醒。
|
||||
|
||||
|
||||
# 第二部分:定义 CardProcessor 子类
|
||||
# 1. 使用 class CardProcessor(PaymentProcessor): 表示继承。
|
||||
# 2. 定义 __init__(self, card_number) 方法,card_number 标注为 str。
|
||||
# 3. 执行 super().__init__("银行卡"),复用父类初始化逻辑。
|
||||
# 4. 保存 self.card_number,只保留卡号最后四位即可:card_number[-4:]。
|
||||
# 5. 重写 pay(self, amount),返回:
|
||||
# “银行卡支付 100.0 元|卡号末四位:1234”。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - Python 3 通常直接写 super(),不需要传当前类和 self;
|
||||
# - super() 按方法解析顺序继续查找方法,不应简单理解为固定的“父类对象”。
|
||||
|
||||
|
||||
# 第三部分:定义 QrCodeProcessor 类,但不要继承 PaymentProcessor
|
||||
# 1. 定义 pay(self, amount) 方法。
|
||||
# 2. 返回“二维码支付 50.0 元”。
|
||||
#
|
||||
# 多态重点:
|
||||
# - 该类没有 extends,也没有 implements;
|
||||
# - 只要它提供调用者需要的 pay(),就能参与统一调用;
|
||||
# - 这种“关注行为而非声明类型”的方式称为鸭子类型(Duck Typing)。
|
||||
|
||||
|
||||
# 第四部分:定义 process_payment(processor, amount) 函数
|
||||
# 1. 调用 processor.pay(amount) 并 return 它的结果。
|
||||
# 2. processor 的类型注解本题可以省略,不作为验收阻塞项。
|
||||
# 3. 函数内部不要使用 isinstance() 判断具体类型。
|
||||
#
|
||||
# Java 对照提醒:
|
||||
# - Java 通常要求 processor 实现统一接口;
|
||||
# - Python 运行时只要求传入对象具有可调用的 pay() 方法。
|
||||
|
||||
|
||||
# 第五部分:定义 main() 并验证两种处理器
|
||||
# 1. 创建 CardProcessor("6222020012341234")。
|
||||
# 2. 创建 QrCodeProcessor()。
|
||||
# 3. 使用 process_payment(card_processor, 100.0) 并输出结果。
|
||||
# 4. 使用 process_payment(qr_processor, 50.0) 并输出结果。
|
||||
# 5. 输出 CardProcessor 的方法解析顺序名称:
|
||||
# [class_type.__name__ for class_type in CardProcessor.mro()]
|
||||
# 6. 添加程序入口判断并调用 main()。
|
||||
#
|
||||
# 预期输出:
|
||||
# 银行卡支付 100.0 元|卡号末四位:1234
|
||||
# 二维码支付 50.0 元
|
||||
# ['CardProcessor', 'PaymentProcessor', 'object']
|
||||
|
||||
|
||||
# 完成后自查:
|
||||
# 1. 是否会使用 super() 复用父类初始化;
|
||||
# 2. 是否知道 Python 方法重写不要求 @Override;
|
||||
# 3. 是否能解释 super() 与 MRO 的关系;
|
||||
# 4. 是否知道未继承同一父类的对象也能参与鸭子类型调用;
|
||||
# 5. 是否避免通过 isinstance() 分支实现伪多态;
|
||||
# 6. 是否只把运行错误、结果错误和关键知识点错误视为阻塞项。
|
||||
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()
|
||||
226
03_面向对象/3_5_面向对象综合项目/README.md
Normal file
226
03_面向对象/3_5_面向对象综合项目/README.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# 第 3-5 课:面向对象综合项目
|
||||
|
||||
## 一、项目目标
|
||||
|
||||
本课完成一个内存版图书管理系统,集中验证第三阶段知识。数据只保存在程序运行期间,不读写文件,也不连接数据库,避免重复第二阶段内容。
|
||||
|
||||
完成后,你将能够:
|
||||
|
||||
1. 从业务职责中识别类;
|
||||
2. 使用 dataclass 定义数据对象;
|
||||
3. 使用普通类组织业务操作;
|
||||
4. 使用组合建立对象关系;
|
||||
5. 使用 property 和特殊方法提供清晰接口;
|
||||
6. 使用自定义异常表达业务失败;
|
||||
7. 使用鸭子类型接入通知能力;
|
||||
8. 对借书、还书和异常流程进行完整验证。
|
||||
|
||||
## 二、项目角色
|
||||
|
||||
| 角色 | 主要职责 |
|
||||
|---|---|
|
||||
| `Book` | 保存图书信息和可借状态,提供友好显示 |
|
||||
| `Member` | 保存会员信息和已借 ISBN,计算已借数量 |
|
||||
| `Library` | 管理图书、会员以及借还业务规则 |
|
||||
| `ConsoleNotifier` | 输出借还成功通知 |
|
||||
| 业务异常 | 区分图书不存在、会员不存在、图书不可借等失败 |
|
||||
|
||||
职责边界很重要:
|
||||
|
||||
- `Book` 不负责查找会员;
|
||||
- `Member` 不负责维护整个图书馆;
|
||||
- `ConsoleNotifier` 不负责改变借阅状态;
|
||||
- `Library` 负责协调对象并保证借还规则一致。
|
||||
|
||||
## 三、为什么 Library 使用普通类
|
||||
|
||||
`Book` 和 `Member` 主要保存数据,适合使用 `@dataclass`。
|
||||
|
||||
`Library` 的主要职责是管理业务行为和对象关系,并不是单纯的数据容器,因此使用普通类更清晰。不是所有类都应该加 `@dataclass`。
|
||||
|
||||
## 四、组合关系
|
||||
|
||||
图书馆“拥有”图书和会员:
|
||||
|
||||
```python
|
||||
self.books: dict[str, Book] = {}
|
||||
self.members: dict[str, Member] = {}
|
||||
```
|
||||
|
||||
这是组合,而不是继承。`Library` 不是一种 `Book`,所以不应写成:
|
||||
|
||||
```python
|
||||
class Library(Book):
|
||||
...
|
||||
```
|
||||
|
||||
也不应为了复用字典操作就让 `Library` 继承 `dict`。把字典作为内部属性更容易维护业务约束。
|
||||
|
||||
## 五、业务异常体系
|
||||
|
||||
项目使用一个共同基础异常:
|
||||
|
||||
```python
|
||||
class LibraryError(Exception):
|
||||
pass
|
||||
```
|
||||
|
||||
具体异常继承它:
|
||||
|
||||
```python
|
||||
class BookNotFoundError(LibraryError):
|
||||
pass
|
||||
```
|
||||
|
||||
调用者既可以捕获具体错误,也可以统一处理所有图书馆业务错误:
|
||||
|
||||
```python
|
||||
try:
|
||||
library.borrow_book(...)
|
||||
except LibraryError as error:
|
||||
print(error)
|
||||
```
|
||||
|
||||
与 Java 不同,Python 没有受检异常的强制 `throws` 声明。
|
||||
|
||||
## 六、借书事务中的一致性
|
||||
|
||||
一次成功借书必须同时更新:
|
||||
|
||||
```text
|
||||
Book.available = False
|
||||
Member.borrowed_isbns 添加 ISBN
|
||||
发送成功通知
|
||||
```
|
||||
|
||||
更新前要先完成全部业务校验。如果先修改一部分状态再发现错误,容易留下不一致数据。
|
||||
|
||||
本课数据都在内存中,因此暂不涉及数据库事务。第四阶段学习数据库后,会把这里的业务模型升级为持久化版本。
|
||||
|
||||
## 七、鸭子类型通知器
|
||||
|
||||
`Library` 不要求通知器继承指定父类,只调用:
|
||||
|
||||
```python
|
||||
notifier.send(message)
|
||||
```
|
||||
|
||||
未来可以传入邮件通知器、短信通知器或测试通知器,只要它们提供兼容的 `send()` 方法。
|
||||
|
||||
不要在业务类中写:
|
||||
|
||||
```python
|
||||
if isinstance(notifier, ConsoleNotifier):
|
||||
...
|
||||
```
|
||||
|
||||
这会让业务类依赖具体实现,降低扩展能力。
|
||||
|
||||
## 八、参考示例
|
||||
|
||||
先运行不同业务场景的仓库示例:
|
||||
|
||||
```powershell
|
||||
python .\03_面向对象\3_5_面向对象综合项目\domain_model_example.py
|
||||
```
|
||||
|
||||
示例展示:
|
||||
|
||||
- dataclass 数据对象;
|
||||
- property 计算状态;
|
||||
- 普通业务类通过组合管理对象;
|
||||
- 自定义异常;
|
||||
- 操作前校验和状态修改。
|
||||
|
||||
示例不是综合项目答案,只用于参考对象职责和代码组织方式。
|
||||
|
||||
## 九、完成综合项目
|
||||
|
||||
打开:
|
||||
|
||||
```text
|
||||
03_面向对象/3_5_面向对象综合项目/practice.py
|
||||
```
|
||||
|
||||
题目已经按实现顺序提供:
|
||||
|
||||
1. 业务异常;
|
||||
2. `Book`;
|
||||
3. `Member`;
|
||||
4. 通知器;
|
||||
5. `Library` 数据结构;
|
||||
6. 添加与注册;
|
||||
7. 内部查找;
|
||||
8. 借书;
|
||||
9. 还书;
|
||||
10. 可借查询;
|
||||
11. 正常流程;
|
||||
12. 异常流程。
|
||||
|
||||
可以直接从 `practice.py` 开始,不必先完整阅读本文。
|
||||
|
||||
## 十、运行方法
|
||||
|
||||
完成代码后,在项目根目录运行:
|
||||
|
||||
```powershell
|
||||
python .\03_面向对象\3_5_面向对象综合项目\practice.py
|
||||
```
|
||||
|
||||
每完成一两个类就运行一次,不要等全部写完再集中排错。
|
||||
|
||||
## 十一、常见错误
|
||||
|
||||
### 11.1 把所有行为放进一个类
|
||||
|
||||
如果 `Library` 同时负责格式化所有对象、直接保存通知历史和处理用户输入,职责会迅速膨胀。让每个对象管理与自己紧密相关的数据和行为。
|
||||
|
||||
### 11.2 使用继承表达拥有关系
|
||||
|
||||
图书馆拥有图书,不代表图书馆是一种图书。拥有关系使用组合。
|
||||
|
||||
### 11.3 校验前修改状态
|
||||
|
||||
先确认会员、图书和可借状态全部合法,再同时更新图书与会员。
|
||||
|
||||
### 11.4 捕获 Exception 隐藏程序错误
|
||||
|
||||
练习中优先捕获 `LibraryError`。直接捕获所有 `Exception` 可能把拼写错误等程序缺陷误当成业务失败。
|
||||
|
||||
### 11.5 为通知器判断具体类型
|
||||
|
||||
直接调用 `notifier.send()`,保持鸭子类型。不要为每种通知方式增加一个条件分支。
|
||||
|
||||
## 十二、验证层级
|
||||
|
||||
完成后按以下层级检查:
|
||||
|
||||
- 必须修复:语法和运行错误、状态不一致、业务规则遗漏、共享借阅列表、具体类型分支;
|
||||
- 建议改进:重复查找、职责混乱、容易误解的结构;
|
||||
- 可选优化:非必要类型注解、说明文字差异、输出美化和格式统一。
|
||||
|
||||
只有“必须修复”会阻止项目和第三阶段验收。
|
||||
|
||||
## 十三、第三阶段知识回顾
|
||||
|
||||
- `self` 显式出现在实例方法定义中;
|
||||
- `__init__` 负责初始化,创建对象不使用 `new`;
|
||||
- 下划线主要表达约定,不是 Java 访问控制的完全替代;
|
||||
- property 保持属性语法并执行方法逻辑;
|
||||
- `super()` 按 MRO 继续查找实现;
|
||||
- 鸭子类型关注对象行为而非声明的共同接口;
|
||||
- dataclass 减少数据对象样板代码;
|
||||
- 特殊方法让对象参与 Python 标准语法;
|
||||
- 组合通常比为了复用而继承更灵活。
|
||||
|
||||
## 十四、阶段验收标准
|
||||
|
||||
- `practice.py` 正常运行;
|
||||
- 正常借还流程状态正确;
|
||||
- 主要异常流程均被阻止;
|
||||
- 类的职责边界清晰;
|
||||
- 能解释项目中继承、组合和鸭子类型分别出现在哪里;
|
||||
- 能说明 dataclass 和普通类的选择原因;
|
||||
- 能说明 Python 面向对象写法与 Java 的主要差异。
|
||||
|
||||
通过后,第三阶段完成,下一阶段开始数据库编程,并逐步把本项目升级为持久化版本。
|
||||
82
03_面向对象/3_5_面向对象综合项目/domain_model_example.py
Normal file
82
03_面向对象/3_5_面向对象综合项目/domain_model_example.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# 第 3-5 课参考示例:小型仓库领域模型
|
||||
#
|
||||
# 本示例使用与综合项目不同的“仓库”场景,演示如何让对象各自负责自己的数据和行为。
|
||||
# 综合项目仍需独立完成,不能直接复制本示例得到答案。
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
class InventoryError(Exception):
|
||||
"""表示仓库业务规则错误。"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Product:
|
||||
"""保存商品数据,并负责与商品自身有关的显示。"""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
quantity: int = 0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""确保创建商品时库存数量合法。"""
|
||||
if self.quantity < 0:
|
||||
raise InventoryError("库存数量不能小于 0。")
|
||||
|
||||
@property
|
||||
def in_stock(self) -> bool:
|
||||
"""根据当前数量计算是否有库存。"""
|
||||
return self.quantity > 0
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""返回面向使用者的商品信息。"""
|
||||
return f"{self.name}|库存:{self.quantity}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Warehouse:
|
||||
"""通过组合管理多个 Product 对象。"""
|
||||
|
||||
name: str
|
||||
products: dict[str, Product] = field(default_factory=dict)
|
||||
|
||||
def add_product(self, product: Product) -> None:
|
||||
"""添加商品,商品编码重复时阻止操作。"""
|
||||
if product.code in self.products:
|
||||
raise InventoryError("商品编码已存在。")
|
||||
self.products[product.code] = product
|
||||
|
||||
def remove_stock(self, code: str, quantity: int) -> None:
|
||||
"""扣减指定商品库存。"""
|
||||
if code not in self.products:
|
||||
raise InventoryError("商品不存在。")
|
||||
|
||||
product = self.products[code]
|
||||
if quantity <= 0:
|
||||
raise InventoryError("出库数量必须大于 0。")
|
||||
if product.quantity < quantity:
|
||||
raise InventoryError("库存不足。")
|
||||
|
||||
product.quantity -= quantity
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""运行仓库领域模型示例。"""
|
||||
warehouse = Warehouse("教学仓库")
|
||||
keyboard = Product("P001", "机械键盘", 3)
|
||||
|
||||
warehouse.add_product(keyboard)
|
||||
print(keyboard)
|
||||
print(f"是否有库存:{keyboard.in_stock}")
|
||||
|
||||
warehouse.remove_stock("P001", 2)
|
||||
print(f"出库后:{keyboard}")
|
||||
|
||||
try:
|
||||
warehouse.remove_stock("P001", 2)
|
||||
except InventoryError as error:
|
||||
print(f"业务操作失败:{error}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
177
03_面向对象/3_5_面向对象综合项目/practice.py
Normal file
177
03_面向对象/3_5_面向对象综合项目/practice.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# 第 3-5 课综合项目:内存版图书管理系统
|
||||
#
|
||||
# 本项目只使用内存数据,不读写文件、不连接数据库。
|
||||
# 请按照题目顺序实现,不要删除业务规则、测试流程、预期输出和验收说明。
|
||||
|
||||
|
||||
# 第一部分:定义业务异常
|
||||
# 1. 定义 LibraryError(Exception),类体中只写 pass。
|
||||
# 2. 定义 BookNotFoundError(LibraryError),类体中只写 pass。
|
||||
# 3. 定义 BookUnavailableError(LibraryError),类体中只写 pass。
|
||||
# 4. 定义 MemberNotFoundError(LibraryError),类体中只写 pass。
|
||||
#
|
||||
# Python 与 Java 对照:
|
||||
# - 自定义异常同样通过继承建立分类;
|
||||
# - Python 没有 Java 受检异常(Checked Exception)的强制 throws 声明;
|
||||
# - 调用者可以捕获具体异常,也可以统一捕获 LibraryError。
|
||||
|
||||
|
||||
# 第二部分:定义 Book 数据类
|
||||
# 1. 使用 @dataclass。
|
||||
# 2. 定义字段:
|
||||
# isbn: str
|
||||
# title: str
|
||||
# author: str
|
||||
# available: bool = True
|
||||
# 3. 定义 __str__,返回格式:
|
||||
# “《Python 编程》|作者:小明|状态:可借”。
|
||||
# 4. available 为 False 时,状态文字应为“已借出”。
|
||||
#
|
||||
# 设计提醒:
|
||||
# - dataclass 自动生成初始化、调试显示和字段相等比较;
|
||||
# - __str__ 只负责一个 Book 自身的友好显示。
|
||||
|
||||
|
||||
# 第三部分:定义 Member 数据类
|
||||
# 1. 使用 @dataclass。
|
||||
# 2. 定义字段:
|
||||
# member_id: str
|
||||
# name: str
|
||||
# borrowed_isbns: list[str] = field(default_factory=list)
|
||||
# 3. 定义 borrowed_count 只读 property,返回已借 ISBN 的数量。
|
||||
#
|
||||
# 设计提醒:
|
||||
# - 使用 default_factory,避免不同会员共享同一借阅列表;
|
||||
# - Member 只保存自己的借阅结果,不负责查找图书馆中的书。
|
||||
|
||||
|
||||
# 第四部分:定义 ConsoleNotifier
|
||||
# 1. 不需要继承任何通知父类或接口。
|
||||
# 2. 定义 send(self, message) 方法,直接输出:
|
||||
# “通知:{message}”。
|
||||
#
|
||||
# 鸭子类型提醒:
|
||||
# - Library 只要求通知器具有 send() 方法;
|
||||
# - 不要在 Library 中使用 isinstance() 判断通知器类型。
|
||||
|
||||
|
||||
# 第五部分:定义 Library 类和初始化方法
|
||||
# 1. 定义 __init__(self, name) 方法并保存 self.name。
|
||||
# 2. 创建实例属性:
|
||||
# self.books: dict[str, Book] = {}
|
||||
# self.members: dict[str, Member] = {}
|
||||
# 3. books 使用 ISBN 作为键,members 使用会员编号作为键。
|
||||
#
|
||||
# 组合提醒:
|
||||
# - Library “拥有”多个 Book 和 Member,因此这里使用组合而不是继承;
|
||||
# - 不要让 Library 继承 list 或 dict。
|
||||
|
||||
|
||||
# 第六部分:实现 add_book(self, book)
|
||||
# 1. book.isbn 已存在时抛出 LibraryError("ISBN 已存在。")。
|
||||
# 2. 不存在时把 book 保存到 self.books。
|
||||
# 3. 本方法不需要输出。
|
||||
|
||||
|
||||
# 第七部分:实现 register_member(self, member)
|
||||
# 1. member.member_id 已存在时抛出 LibraryError("会员编号已存在。")。
|
||||
# 2. 不存在时把 member 保存到 self.members。
|
||||
# 3. 本方法不需要输出。
|
||||
|
||||
|
||||
# 第八部分:实现两个内部查找方法
|
||||
# 1. 定义 _get_book(self, isbn),找到时返回 Book。
|
||||
# 2. 找不到时抛出 BookNotFoundError("图书不存在。")。
|
||||
# 3. 定义 _get_member(self, member_id),找到时返回 Member。
|
||||
# 4. 找不到时抛出 MemberNotFoundError("会员不存在。")。
|
||||
#
|
||||
# 单下划线提醒:
|
||||
# - _get_book 和 _get_member 表示 Library 内部使用的辅助方法;
|
||||
# - 单下划线是约定,不是 Java private 式的强制限制。
|
||||
|
||||
|
||||
# 第九部分:实现 borrow_book(self, member_id, isbn, notifier)
|
||||
# 1. 通过 _get_member() 查找会员。
|
||||
# 2. 通过 _get_book() 查找图书。
|
||||
# 3. book.available 为 False 时抛出:
|
||||
# BookUnavailableError("图书已被借出。")。
|
||||
# 4. 校验通过后执行:
|
||||
# book.available = False
|
||||
# member.borrowed_isbns.append(isbn)
|
||||
# 5. 调用 notifier.send(),消息格式:
|
||||
# “小明成功借阅《Python 编程》”。
|
||||
#
|
||||
# 多态提醒:
|
||||
# - notifier 的类型注解可以省略,不影响验收;
|
||||
# - 只调用 send(),不要判断通知器的具体类型。
|
||||
|
||||
|
||||
# 第十部分:实现 return_book(self, member_id, isbn, notifier)
|
||||
# 1. 通过两个内部查找方法取得会员和图书。
|
||||
# 2. isbn 不在 member.borrowed_isbns 中时抛出:
|
||||
# LibraryError("该会员没有借阅这本书。")。
|
||||
# 3. 校验通过后执行:
|
||||
# member.borrowed_isbns.remove(isbn)
|
||||
# book.available = True
|
||||
# 4. 调用 notifier.send(),消息格式:
|
||||
# “小明成功归还《Python 编程》”。
|
||||
|
||||
|
||||
# 第十一部分:实现 available_books(self)
|
||||
# 1. 返回所有 available 为 True 的 Book 对象组成的新列表。
|
||||
# 2. 不要修改 self.books。
|
||||
# 3. 可以使用列表推导式。
|
||||
|
||||
|
||||
# 第十二部分:定义 main() 并完成正常流程
|
||||
# 1. 创建 Library("城市图书馆")。
|
||||
# 2. 创建 ConsoleNotifier()。
|
||||
# 3. 添加以下两本书:
|
||||
# Book("978-001", "Python 编程", "小明")
|
||||
# Book("978-002", "流畅的 Python", "Luciano")
|
||||
# 4. 注册 Member("M001", "小红")。
|
||||
# 5. 调用 library.available_books() 取得当前可借图书列表。
|
||||
# 6. 使用 len() 统计列表长度并输出“初始可借数量:2”,参考写法:
|
||||
# print(f"初始可借数量:{len(library.available_books())}")
|
||||
# 7. 借出 ISBN 为 978-001 的书。
|
||||
# 8. 再次调用 available_books(),使用 len() 输出“借阅后可借数量:1”。
|
||||
# 9. 读取前面创建的 Member 对象的 borrowed_count,输出“会员已借数量:1”。
|
||||
# 10. 归还 ISBN 为 978-001 的书。
|
||||
# 11. 再次调用 available_books(),使用 len() 输出“归还后可借数量:2”。
|
||||
# 12. 再次读取该 Member 对象的 borrowed_count,输出“会员已借数量:0”。
|
||||
|
||||
|
||||
# 第十三部分:在 main() 中完成异常流程
|
||||
# 1. 再次借出 978-001。
|
||||
# 2. 使用 try...except 再借一次同一本书。
|
||||
# 3. 捕获 LibraryError,并输出:
|
||||
# “重复借阅已被阻止:图书已被借出。”。
|
||||
# 4. 使用 try...except 借阅 ISBN 为 978-999 的书。
|
||||
# 5. 捕获 LibraryError,并输出:
|
||||
# “不存在的图书已被阻止:图书不存在。”。
|
||||
# 6. 添加程序入口判断并调用 main()。
|
||||
#
|
||||
# 完整预期输出:
|
||||
# 初始可借数量:2
|
||||
# 通知:小红成功借阅《Python 编程》
|
||||
# 借阅后可借数量:1
|
||||
# 会员已借数量:1
|
||||
# 通知:小红成功归还《Python 编程》
|
||||
# 归还后可借数量:2
|
||||
# 会员已借数量:0
|
||||
# 通知:小红成功借阅《Python 编程》
|
||||
# 重复借阅已被阻止:图书已被借出。
|
||||
# 不存在的图书已被阻止:图书不存在。
|
||||
|
||||
|
||||
# 最终验收标准:
|
||||
# 1. Book 和 Member 使用 dataclass,且不同会员不共享借阅列表;
|
||||
# 2. Book 的 __str__ 能区分“可借”和“已借出”;
|
||||
# 3. Member.borrowed_count 能随借还操作变化;
|
||||
# 4. Library 通过组合管理 Book 和 Member;
|
||||
# 5. 借书和还书同时更新图书状态与会员借阅记录;
|
||||
# 6. 通知器通过鸭子类型调用,没有具体类型判断;
|
||||
# 7. 业务异常继承关系正确,并能统一捕获 LibraryError;
|
||||
# 8. 重复 ISBN、重复会员、图书不存在、会员不存在、重复借阅和错误归还均被阻止;
|
||||
# 9. available_books() 返回新列表,不修改图书字典;
|
||||
# 10. 程序实际输出的业务结果正确;说明文字或非必要类型注解不阻塞验收。
|
||||
34
README.md
34
README.md
@@ -186,6 +186,11 @@ Python/
|
||||
│ ├── 2_8_虚拟环境与依赖管理/
|
||||
│ └── 2_9_python进阶综合项目/
|
||||
├── 03_面向对象/
|
||||
│ ├── 3_1_Python与Java的类和对象/
|
||||
│ ├── 3_2_Python与Java的封装差异/
|
||||
│ ├── 3_3_Python与Java的继承和多态差异/
|
||||
│ ├── 3_4_Python特殊方法与数据类/
|
||||
│ └── 3_5_面向对象综合项目/
|
||||
├── 04_数据库/
|
||||
├── 05_web基础/
|
||||
├── 06_fastapi/
|
||||
@@ -197,13 +202,13 @@ Python/
|
||||
|
||||
## 当前学习进度
|
||||
|
||||
- 当前阶段:第二阶段——Python 进阶。
|
||||
- 当前课程:`2_9_python进阶综合项目`。
|
||||
- 当前状态:正在学习第二阶段综合项目。
|
||||
- 已完成课程:`1_1_hello_world` 至 `1_14_python基础综合项目`,以及第二阶段 `2_1_模块与包` 至 `2_8_虚拟环境与依赖管理`。
|
||||
- 学习中的课程:`2_9_python进阶综合项目`。
|
||||
- 已创建课程目录:第一阶段全部课程,以及第二阶段 `2_1_模块与包` 至 `2_9_python进阶综合项目`。
|
||||
- 下一步:完成综合项目并进行第二阶段验收。
|
||||
- 当前阶段:第三阶段——面向对象编程。
|
||||
- 当前课程:`3_5_面向对象综合项目`。
|
||||
- 当前状态:第三阶段已完成并通过综合项目验收。
|
||||
- 已完成课程:第一阶段 `1_1_hello_world` 至 `1_14_python基础综合项目`、第二阶段 `2_1_模块与包` 至 `2_9_python进阶综合项目`,以及第三阶段 `3_1` 至 `3_5`。
|
||||
- 学习中的课程:无。
|
||||
- 已创建课程目录:第一阶段全部课程、第二阶段全部课程,以及第三阶段 `3_1` 至 `3_5`。
|
||||
- 下一步:进入第四阶段数据库编程,从数据库与关系型数据库基本概念开始。
|
||||
|
||||
## 建议环境
|
||||
|
||||
@@ -218,12 +223,13 @@ Python/
|
||||
|
||||
## 下一步
|
||||
|
||||
当前开始第二阶段综合项目,综合使用:
|
||||
第三阶段已完成,已经掌握:
|
||||
|
||||
1. 模块与包;
|
||||
2. 文件读写与异常处理;
|
||||
3. 推导式、生成器和类型注解;
|
||||
4. 使用多个文件组织任务管理程序;
|
||||
5. 保存并恢复任务数据。
|
||||
1. 使用 dataclass 建模图书和会员;
|
||||
2. 使用普通业务类协调借书与还书;
|
||||
3. 通过组合管理对象关系;
|
||||
4. 使用自定义异常表达业务失败;
|
||||
5. 使用鸭子类型接入通知器;
|
||||
6. 验证正常流程和主要异常流程。
|
||||
|
||||
完成练习后,可进行第二阶段验收,再进入第三阶段“面向对象编程”。
|
||||
下一步进入第四阶段数据库编程,将逐步学习 SQL,并把内存版图书管理系统升级为持久化版本。
|
||||
|
||||
Reference in New Issue
Block a user