From f1d9548646ab522e6e7bfa861ae96251496ed2f4 Mon Sep 17 00:00:00 2001 From: "zhiye.sun" Date: Tue, 11 Aug 2026 17:53:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=9D=A2=E5=90=91=E5=AF=B9=E8=B1=A1):=20?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=AC=AC=E4=B8=89=E9=98=B6=E6=AE=B5=E5=B7=AE?= =?UTF-8?q?=E5=BC=82=E5=8C=96=E8=AF=BE=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../3_1_Python与Java的类和对象/README.md | 259 +++++++++++++++++ .../class_object_comparison.py | 63 +++++ .../3_1_Python与Java的类和对象/practice.py | 82 ++++++ .../3_2_Python与Java的封装差异/README.md | 244 ++++++++++++++++ .../encapsulation_comparison.py | 69 +++++ .../3_2_Python与Java的封装差异/practice.py | 70 +++++ .../3_3_Python与Java的继承和多态差异/README.md | 242 ++++++++++++++++ .../inheritance_polymorphism_comparison.py | 78 +++++ .../3_3_Python与Java的继承和多态差异/practice.py | 72 +++++ .../3_4_Python特殊方法与数据类/README.md | 266 ++++++++++++++++++ .../3_4_Python特殊方法与数据类/practice.py | 87 ++++++ .../special_methods_dataclass_example.py | 94 +++++++ 03_面向对象/3_5_面向对象综合项目/README.md | 226 +++++++++++++++ .../3_5_面向对象综合项目/domain_model_example.py | 82 ++++++ 03_面向对象/3_5_面向对象综合项目/practice.py | 177 ++++++++++++ README.md | 34 ++- 16 files changed, 2131 insertions(+), 14 deletions(-) create mode 100644 03_面向对象/3_1_Python与Java的类和对象/README.md create mode 100644 03_面向对象/3_1_Python与Java的类和对象/class_object_comparison.py create mode 100644 03_面向对象/3_1_Python与Java的类和对象/practice.py create mode 100644 03_面向对象/3_2_Python与Java的封装差异/README.md create mode 100644 03_面向对象/3_2_Python与Java的封装差异/encapsulation_comparison.py create mode 100644 03_面向对象/3_2_Python与Java的封装差异/practice.py create mode 100644 03_面向对象/3_3_Python与Java的继承和多态差异/README.md create mode 100644 03_面向对象/3_3_Python与Java的继承和多态差异/inheritance_polymorphism_comparison.py create mode 100644 03_面向对象/3_3_Python与Java的继承和多态差异/practice.py create mode 100644 03_面向对象/3_4_Python特殊方法与数据类/README.md create mode 100644 03_面向对象/3_4_Python特殊方法与数据类/practice.py create mode 100644 03_面向对象/3_4_Python特殊方法与数据类/special_methods_dataclass_example.py create mode 100644 03_面向对象/3_5_面向对象综合项目/README.md create mode 100644 03_面向对象/3_5_面向对象综合项目/domain_model_example.py create mode 100644 03_面向对象/3_5_面向对象综合项目/practice.py diff --git a/03_面向对象/3_1_Python与Java的类和对象/README.md b/03_面向对象/3_1_Python与Java的类和对象/README.md new file mode 100644 index 0000000..d771823 --- /dev/null +++ b/03_面向对象/3_1_Python与Java的类和对象/README.md @@ -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` 的实际输出与题目预期一致。 diff --git a/03_面向对象/3_1_Python与Java的类和对象/class_object_comparison.py b/03_面向对象/3_1_Python与Java的类和对象/class_object_comparison.py new file mode 100644 index 0000000..0f077fe --- /dev/null +++ b/03_面向对象/3_1_Python与Java的类和对象/class_object_comparison.py @@ -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() diff --git a/03_面向对象/3_1_Python与Java的类和对象/practice.py b/03_面向对象/3_1_Python与Java的类和对象/practice.py new file mode 100644 index 0000000..bae838f --- /dev/null +++ b/03_面向对象/3_1_Python与Java的类和对象/practice.py @@ -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. 是否没有在类外直接替学习者写出答案。 diff --git a/03_面向对象/3_2_Python与Java的封装差异/README.md b/03_面向对象/3_2_Python与Java的封装差异/README.md new file mode 100644 index 0000000..ae44a60 --- /dev/null +++ b/03_面向对象/3_2_Python与Java的封装差异/README.md @@ -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 的调用差异。 diff --git a/03_面向对象/3_2_Python与Java的封装差异/encapsulation_comparison.py b/03_面向对象/3_2_Python与Java的封装差异/encapsulation_comparison.py new file mode 100644 index 0000000..4a72f06 --- /dev/null +++ b/03_面向对象/3_2_Python与Java的封装差异/encapsulation_comparison.py @@ -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() diff --git a/03_面向对象/3_2_Python与Java的封装差异/practice.py b/03_面向对象/3_2_Python与Java的封装差异/practice.py new file mode 100644 index 0000000..d9f243b --- /dev/null +++ b/03_面向对象/3_2_Python与Java的封装差异/practice.py @@ -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. 是否只把运行错误和逻辑错误视为阻塞项,不把可选类型注解当作必须项。 diff --git a/03_面向对象/3_3_Python与Java的继承和多态差异/README.md b/03_面向对象/3_3_Python与Java的继承和多态差异/README.md new file mode 100644 index 0000000..1f20aa4 --- /dev/null +++ b/03_面向对象/3_3_Python与Java的继承和多态差异/README.md @@ -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` 输出正确支付结果。 diff --git a/03_面向对象/3_3_Python与Java的继承和多态差异/inheritance_polymorphism_comparison.py b/03_面向对象/3_3_Python与Java的继承和多态差异/inheritance_polymorphism_comparison.py new file mode 100644 index 0000000..ff1a27c --- /dev/null +++ b/03_面向对象/3_3_Python与Java的继承和多态差异/inheritance_polymorphism_comparison.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() diff --git a/03_面向对象/3_3_Python与Java的继承和多态差异/practice.py b/03_面向对象/3_3_Python与Java的继承和多态差异/practice.py new file mode 100644 index 0000000..2e4760f --- /dev/null +++ b/03_面向对象/3_3_Python与Java的继承和多态差异/practice.py @@ -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. 是否只把运行错误、结果错误和关键知识点错误视为阻塞项。 diff --git a/03_面向对象/3_4_Python特殊方法与数据类/README.md b/03_面向对象/3_4_Python特殊方法与数据类/README.md new file mode 100644 index 0000000..ce6ea4e --- /dev/null +++ b/03_面向对象/3_4_Python特殊方法与数据类/README.md @@ -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` 的购物车计算与独立列表验证正确。 diff --git a/03_面向对象/3_4_Python特殊方法与数据类/practice.py b/03_面向对象/3_4_Python特殊方法与数据类/practice.py new file mode 100644 index 0000000..9d88a3e --- /dev/null +++ b/03_面向对象/3_4_Python特殊方法与数据类/practice.py @@ -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. 是否只把运行错误、结果错误和关键知识点错误视为阻塞项。 diff --git a/03_面向对象/3_4_Python特殊方法与数据类/special_methods_dataclass_example.py b/03_面向对象/3_4_Python特殊方法与数据类/special_methods_dataclass_example.py new file mode 100644 index 0000000..4afb385 --- /dev/null +++ b/03_面向对象/3_4_Python特殊方法与数据类/special_methods_dataclass_example.py @@ -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() diff --git a/03_面向对象/3_5_面向对象综合项目/README.md b/03_面向对象/3_5_面向对象综合项目/README.md new file mode 100644 index 0000000..cd0278b --- /dev/null +++ b/03_面向对象/3_5_面向对象综合项目/README.md @@ -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 的主要差异。 + +通过后,第三阶段完成,下一阶段开始数据库编程,并逐步把本项目升级为持久化版本。 diff --git a/03_面向对象/3_5_面向对象综合项目/domain_model_example.py b/03_面向对象/3_5_面向对象综合项目/domain_model_example.py new file mode 100644 index 0000000..a8b36f6 --- /dev/null +++ b/03_面向对象/3_5_面向对象综合项目/domain_model_example.py @@ -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() diff --git a/03_面向对象/3_5_面向对象综合项目/practice.py b/03_面向对象/3_5_面向对象综合项目/practice.py new file mode 100644 index 0000000..14728b4 --- /dev/null +++ b/03_面向对象/3_5_面向对象综合项目/practice.py @@ -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. 程序实际输出的业务结果正确;说明文字或非必要类型注解不阻塞验收。 diff --git a/README.md b/README.md index a5158b1..15de367 100644 --- a/README.md +++ b/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,并把内存版图书管理系统升级为持久化版本。