feat(面向对象): 完成第三阶段差异化课程
This commit is contained in:
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. 是否只把运行错误和逻辑错误视为阻塞项,不把可选类型注解当作必须项。
|
||||
Reference in New Issue
Block a user