# 第 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 的调用差异。