第55集:封装特性

学习目标

  • 理解封装的概念和作用
  • 掌握Python中的访问修饰符(public、protected、private)
  • 学会使用属性装饰器(@property)
  • 理解getter和setter方法
  • 掌握数据验证和访问控制
  • 学会设计良好的接口

一、封装概述

1.1 什么是封装

封装:将对象的属性和方法包装在一起,隐藏内部实现细节,只暴露必要的接口。

1.2 封装的作用

┌─────────────────────────────────────┐
│          外部世界                   │
└─────────────────────────────────────┘
              ↓
┌─────────────────────────────────────┐
│        公共接口 (Public)             │ ← 只暴露必要的
├─────────────────────────────────────┤
│        受保护的接口 (Protected)      │
├─────────────────────────────────────┤
│        私有实现 (Private)            │ ← 隐藏内部细节
└─────────────────────────────────────┘

封装的好处:

  1. 保护数据安全
  2. 隐藏实现细节
  3. 降低耦合度
  4. 提高可维护性
  5. 便于修改和扩展

1.3 封装示例

class BankAccount:
    def __init__(self, account_number, initial_balance):
        self.account_number = account_number
        self.__balance = initial_balance  # 私有属性
    
    def deposit(self, amount):
        """存款 - 公共接口"""
        if amount > 0:
            self.__balance += amount
            return True
        return False
    
    def withdraw(self, amount):
        """取款 - 公共接口"""
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            return True
        return False
    
    def get_balance(self):
        """查询余额 - 公共接口"""
        return self.__balance

# 使用
account = BankAccount("001", 1000)
account.deposit(500)
account.withdraw(200)
print(account.get_balance())  # 输出:1300
# 不能直接访问 account.__balance

二、Python中的访问修饰符

2.1 公共属性(Public)

特征:

  • 没有特殊前缀
  • 可以在类外部直接访问
  • 没有任何访问限制

示例:

class Person:
    def __init__(self, name, age):
        self.name = name    # 公共属性
        self.age = age      # 公共属性

p = Person("张三", 18)
print(p.name)  # 直接访问
p.age = 20     # 直接修改

2.2 受保护的属性(Protected)

特征:

  • 以单个下划线_开头
  • 约定俗成的私有,实际上可以访问
  • 表示"不建议在类外部访问"

示例:

class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age      # 受保护的属性

p = Person("张三", 18)
print(p._age)  # 可以访问,但不建议
p._age = 20    # 可以修改,但不建议

2.3 私有属性(Private)

特征:

  • 以双下划线__开头
  • Python会进行名称改写(name mangling)
  • 外部不能直接访问

示例:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.__age = age  # 私有属性

p = Person("张三", 18)
# print(p.__age)  # 错误:AttributeError
# p.__age = 20    # 错误:创建了一个新属性

名称改写机制:

class Person:
    def __init__(self):
        self.__private = "私有"

p = Person()
# Python将其改写为:_Person__private
print(p._Person__private)  # 可以访问,但不推荐

三、属性装饰器(@property)

3.1 基本用法

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    @property
    def name(self):
        """getter方法"""
        return self._name
    
    @name.setter
    def name(self, value):
        """setter方法"""
        if isinstance(value, str):
            self._name = value
        else:
            raise ValueError("姓名必须是字符串")

p = Person("张三", 18)
print(p.name)  # 调用getter
p.name = "李四"  # 调用setter

3.2 只读属性

class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    @property
    def radius(self):
        return self._radius
    
    @property
    def area(self):
        """只读属性:计算面积"""
        return 3.14159 * self._radius ** 2

c = Circle(5)
print(c.radius)  # 输出:5
print(c.area)    # 输出:78.53975

# c.area = 100  # 错误:只读属性不能赋值

3.3 带验证的属性

class Student:
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score
    
    @property
    def score(self):
        return self._score
    
    @score.setter
    def score(self, value):
        """验证分数"""
        if not isinstance(value, (int, float)):
            raise TypeError("分数必须是数字")
        if value < 0 or value > 100:
            raise ValueError("分数必须在0-100之间")
        self._score = value

s = Student("张三", 18, 85)
print(s.score)  # 输出:85
s.score = 90    # 正常修改

try:
    s.score = 150  # 会报错
except ValueError as e:
    print(e)  # 输出:分数必须在0-100之间

四、完整封装示例

4.1 银行账户类

class BankAccount:
    def __init__(self, account_number, owner, initial_balance=0):
        """初始化账户"""
        self.account_number = account_number  # 公共属性
        self.owner = owner                    # 公共属性
        self.__balance = initial_balance      # 私有属性
        self._min_balance = 100               # 受保护的常量
        self.__transactions = []              # 私有列表
    
    @property
    def balance(self):
        """获取余额"""
        return self.__balance
    
    def deposit(self, amount):
        """存款"""
        if amount <= 0:
            raise ValueError("存款金额必须大于0")
        self.__balance += amount
        self.__add_transaction("存款", amount)
        return True
    
    def withdraw(self, amount):
        """取款"""
        if amount <= 0:
            raise ValueError("取款金额必须大于0")
        if self.__balance - amount < self._min_balance:
            raise ValueError("余额不足")
        self.__balance -= amount
        self.__add_transaction("取款", -amount)
        return True
    
    def __add_transaction(self, type_, amount):
        """私有方法:添加交易记录"""
        self.__transactions.append({
            "type": type_,
            "amount": amount,
            "balance": self.__balance
        })
    
    def get_transactions(self):
        """获取交易记录"""
        return self.__transactions.copy()

# 使用
account = BankAccount("6222000012345678", "张三", 1000)
account.deposit(500)
account.withdraw(200)

print(f"余额: {account.balance}")
print(f"交易记录: {account.get_transactions()}")

4.2 温度转换类

class Temperature:
    def __init__(self, celsius):
        self.__celsius = celsius
    
    @property
    def celsius(self):
        """获取摄氏度"""
        return self.__celsius
    
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("温度不能低于绝对零度")
        self.__celsius = value
    
    @property
    def fahrenheit(self):
        """获取华氏度"""
        return self.__celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self.__celsius = (value - 32) * 5/9
    
    @property
    def kelvin(self):
        """获取开尔文"""
        return self.__celsius + 273.15

temp = Temperature(25)
print(f"摄氏度: {temp.celsius}")
print(f"华氏度: {temp.fahrenheit}")
print(f"开尔文: {temp.kelvin}")

# 通过不同单位设置
temp.fahrenheit = 86
print(f"设置华氏度86后,摄氏度: {temp.celsius}")

五、设计良好的接口

5.1 接口设计原则

  1. 最小化公共接口:只暴露必要的方法和属性
  2. 隐藏实现细节:使用私有属性封装内部逻辑
  3. 提供清晰的文档:说明每个方法和属性的用途
  4. 进行参数验证:确保数据的正确性

5.2 示例:设计良好的类

class EmailValidator:
    """邮箱验证器"""
    
    def __init__(self):
        self.__valid_domains = set()  # 有效的域名
    
    def add_valid_domain(self, domain):
        """添加有效域名"""
        if isinstance(domain, str) and domain:
            self.__valid_domains.add(domain)
    
    def is_valid(self, email):
        """验证邮箱"""
        if not isinstance(email, str):
            return False
        
        if "@" not in email:
            return False
        
        parts = email.split("@")
        if len(parts) != 2:
            return False
        
        local, domain = parts
        if not local or not domain:
            return False
        
        return domain in self.__valid_domains

validator = EmailValidator()
validator.add_valid_domain("gmail.com")
validator.add_valid_domain("163.com")

print(validator.is_valid("user@gmail.com"))  # 输出:True
print(validator.is_valid("user@invalid.com"))  # 输出:False

六、常见错误与注意事项

6.1 常见错误

  1. 过度封装:
class BadExample:
    def __init__(self):
        self.__value = 10
    
    def get_value(self):
        return self.__value
    
    def set_value(self, value):
        self.__value = value

# 不必要的getter和setter
# 应该使用@property
  1. 忘记使用名称改写:
class Person:
    def __init__(self):
        self.__age = 18

p = Person()
# print(p.__age)  # 错误:AttributeError
# 应该使用:print(p._Person__age)

6.2 注意事项

  1. Python的私有不是绝对的:通过名称改写仍然可以访问
  2. 私有主要用于提醒开发者:遵循约定可以提高代码质量
  3. 使用@property实现更好的封装:比getter/setter更优雅
  4. 适当使用保护级别:不是所有属性都需要私有

七、课后练习

练习题

  1. 编程题

    • 定义一个Rectangle
    • 使用私有属性存储长和宽
    • 使用@property实现属性访问
    • 添加面积和周长的只读属性
  2. 编程题

    • 定义一个Student
    • 封装姓名、年龄、成绩等属性
    • 添加数据验证
    • 计算平均分和等级
  3. 思考题

    • 为什么要使用封装?
    • 什么时候应该使用私有属性?
    • @property和getter/setter有什么区别?

八、本集总结

核心知识点回顾

  1. 封装概念:隐藏实现细节,暴露公共接口
  2. 访问修饰符:public、protected(_)、private(__)
  3. @property:实现优雅的属性访问
  4. 数据验证:在setter中验证数据
  5. 接口设计:最小化公共接口

下集预告

下一集我们将学习继承基础,深入了解如何通过继承实现代码复用。


学习建议: 理解封装的思想,合理使用访问修饰符和@property。通过实际编程练习,掌握良好的封装设计。

« 上一篇 实例方法与self 下一篇 » 继承基础