Day 24 - 私有属性与封装

什么是封装?

封装(Encapsulation)是面向对象编程的三大基本特性之一(另外两个是继承和多态)。封装的核心理念是将数据(属性)和操作数据的方法(行为)打包在一起,对外隐藏内部实现细节,只暴露必要的接口。这样做的好处是:提高代码的可维护性、降低模块间的耦合度、保护数据不被意外或恶意修改。

在 Python 中,封装并不是像 Java 或 C++ 那样通过严格的访问控制关键字(public、private、protected)来实现的。Python 采用了一种"约定优于配置"的方式,通过命名规范来标识成员的访问级别。

Python 的命名规范

Python 中以下划线的前缀和数量来表示属性的访问级别:

  1. _name(单下划线):受保护的属性,按照约定,外部代码不应该直接访问,但 Python 不会阻止这种访问。这是一种"软私有"的约定。

  2. __name(双下划线开头):名称改写(Name Mangling)机制,会将属性名改写为 _类名__name 的形式。这提供了一定程度的"硬私有"保护,但通过改写后的名称仍然可以访问。

  3. __name__(双下划线开头和结尾):这是魔术方法/属性的保留命名空间,如 __init____str____dict__ 等。普通属性不应该使用这种命名方式。

class BankAccount:
    def __init__(self, account_id, initial_balance):
        self.account_id = account_id      # 公开属性
        self._transactions = []            # 受保护属性(约定不直接访问)
        self.__balance = initial_balance   # 私有属性(名称改写)
    
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("存款金额必须为正数")
        self.__balance += amount
        self._transactions.append(("存款", amount))
    
    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("取款金额必须为正数")
        if amount > self.__balance:
            raise ValueError("余额不足")
        self.__balance -= amount
        self._transactions.append(("取款", -amount))
    
    def get_balance(self):
        """获取余额(受控访问)"""
        return self.__balance
    
    def get_transactions(self):
        """获取交易记录"""
        return self._transactions.copy()

# 创建账户
account = BankAccount("ACC001", 1000)

# 公开属性可以直接访问
print(f"账号:{account.account_id}")

# 受保护属性(可以访问,但不推荐)
print(f"交易记录:{account._transactions}")

# 私有属性(访问会报错)
# print(account.__balance)  # AttributeError

# 但可以通过改写后的名称访问(不推荐)
print(account._BankAccount__balance)  # 1000

# 通过方法访问私有属性(推荐方式)
print(f"余额:{account.get_balance()}")

# 存款和取款
account.deposit(500)
account.withdraw(200)
print(f"交易记录:{account.get_transactions()}")
# [('存款', 500), ('取款', -200)]

为什么要使用私有属性?

假设我们有一个 BankAccount 类,余额是一个私有属性。如果允许外部代码直接访问和修改余额,那么可能会出现以下问题:

  1. 数据不一致:余额可能被修改成负数
  2. 业务逻辑被绕过:取款时没有检查余额是否充足
  3. 无法追踪变更:直接修改余额时无法记录交易历史

通过将余额设为私有属性,并提供 depositwithdrawget_balance 等公共方法,我们可以确保所有对余额的修改都经过适当的验证和记录。

# 危险的代码:直接暴露余额
class UnsafeBankAccount:
    def __init__(self, balance):
        self.balance = balance  # 余额是公开的,可以被随意修改

# 可能造成的问题
account = UnsafeBankAccount(1000)
account.balance = -5000  # 余额变成负数(不合理)
account.balance = "一千元"  # 类型错误

属性访问器(Getter 和 Setter)

在 Java 等语言中,通常使用 getter 和 setter 方法来访问和修改属性。Python 也能这样做,但这不是 Pythonic(符合 Python 风格)的方式。不过,有些情况下使用 getter 和 setter 确实很有用,例如:

  1. 在获取或设置属性时执行额外的逻辑
  2. 提供只读或只写的属性
  3. 实现计算属性(computed properties)
class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius
    
    def get_celsius(self):
        """获取摄氏温度"""
        return self._celsius
    
    def set_celsius(self, value):
        """设置摄氏温度"""
        if value < -273.15:
            raise ValueError("温度不能低于绝对零度")
        self._celsius = value
    
    # 使用 property 函数创建属性
    celsius = property(get_celsius, set_celsius)
    
    @property
    def fahrenheit(self):
        """计算属性:华氏温度"""
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        """设置华氏温度"""
        self._celsius = (value - 32) * 5/9

# 使用
temp = Temperature(25)
print(f"摄氏温度:{temp.celsius}°C")      # 25°C
print(f"华氏温度:{temp.fahrenheit}°F")    # 77.0°F

temp.celsius = 30
print(f"摄氏温度:{temp.celsius}°C")      # 30°C
print(f"华氏温度:{temp.fahrenheit}°F")    # 86.0°F

temp.fahrenheit = 68
print(f"摄氏温度:{temp.celsius}°C")      # 20°C
print(f"华氏温度:{temp.fahrenheit}°F")    # 68.0°F

# 验证绝对零度
try:
    temp.celsius = -300  # 抛出异常
except ValueError as e:
    print(f"错误:{e}")  # 温度不能低于绝对零度

@property 装饰器

Python 提供了一个更优雅的方式来创建属性访问器:@property 装饰器。使用 @property 装饰器可以将类方法转换为同名属性的 getter 方法,代码更简洁、更符合 Python 风格。

class Circle:
    def __init__(self, radius):
        self.radius = radius  # 这会调用 setter
    
    @property
    def radius(self):
        """获取半径"""
        return self._radius
    
    @radius.setter
    def radius(self, value):
        """设置半径(带验证)"""
        if value <= 0:
            raise ValueError("半径必须为正数")
        self._radius = value
    
    @property
    def diameter(self):
        """计算属性:直径"""
        return self._radius * 2
    
    @property
    def area(self):
        """计算属性:面积"""
        import math
        return math.pi * self._radius ** 2
    
    @property
    def circumference(self):
        """计算属性:周长"""
        import math
        return 2 * math.pi * self._radius

# 使用
circle = Circle(5)
print(f"半径:{circle.radius}")           # 5
print(f"直径:{circle.diameter}")          # 10
print(f"面积:{circle.area:.2f}")         # 78.54
print(f"周长:{circle.circumference:.2f}") # 31.42

circle.radius = 10
print(f"新半径:{circle.radius}")         # 10
print(f"新面积:{circle.area:.2f}")       # 314.16

try:
    circle.radius = -5  # 抛出异常
except ValueError as e:
    print(f"错误:{e}")  # 半径必须为正数

只读属性

通过只定义 getter 而不定义 setter,可以创建只读属性。

class Person:
    def __init__(self, name, birth_year):
        self.name = name
        self._birth_year = birth_year
    
    @property
    def name(self):
        """姓名(可读写)"""
        return self._name
    
    @name.setter
    def name(self, value):
        if not value:
            raise ValueError("姓名不能为空")
        self._name = value
    
    @property
    def birth_year(self):
        """出生年份(只读)"""
        return self._birth_year
    
    @property
    def age(self):
        """年龄(只读计算属性)"""
        import datetime
        current_year = datetime.datetime.now().year
        return current_year - self._birth_year

# 使用
person = Person("张三", 1990)
print(f"姓名:{person.name}")      # 张三
print(f"出生年份:{person.birth_year}")  # 1990
print(f"年龄:{person.age}")      # 33 或 34(取决于当前年份)

person.name = "李四"  # 可以修改
print(f"新姓名:{person.name}")  # 李四

# person.birth_year = 1995  # AttributeError: property 'birth_year' has no setter

受保护属性的使用场景

受保护属性(单下划线前缀)通常用于"子类可以使用,但外部代码不应直接访问"的场景。这是一种设计决策,表示该属性是类的内部实现细节,可能会在未来的版本中发生变化。

class Animal:
    def __init__(self, name, age):
        self.name = name
        self._age = age  # 受保护,子类可以使用
    
    def speak(self):
        raise NotImplementedError("子类必须实现 speak 方法")
    
    def _display_info(self):  # 受保护的方法
        """子类可以调用这个方法"""
        print(f"动物:{self.name},年龄:{self._age}")

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)
        self._breed = breed  # 子类的受保护属性
    
    def speak(self):
        print(f"{self.name} 在叫:汪汪汪!")
    
    def display(self):
        self._display_info()  # 调用父类的受保护方法
        print(f"品种:{self._breed}")

# 使用
dog = Dog("旺财", 3, "金毛")
dog.speak()           # 旺财 在叫:汪汪汪!
dog.display()
# 动物:旺财,年龄:3
# 品种:金毛

# 外部代码可以直接访问受保护属性(不推荐)
print(f"年龄:{dog._age}")

私有属性的继承与访问

私有属性(双下划线前缀)使用了名称改写机制,在子类中也无法直接通过原名称访问,但可以通过改写后的名称访问。这种设计提供了一定程度的保护。

class Base:
    def __init__(self):
        self.__secret = 42  # 私有属性,改写为 _Base__secret
    
    def get_secret(self):
        return self.__secret  # 类内部可以访问

class Derived(Base):
    def __init__(self):
        super().__init__()
        self.__another = 100  # 私有属性,改写为 _Derived__another
    
    def access_base_secret(self):
        # 可以通过方法访问父类的私有属性
        return self.get_secret()
    
    def get_own_secret(self):
        return self.__another  # 可以访问自己的私有属性

obj = Derived()
print(obj.access_base_secret())  # 42
print(obj.get_own_secret())     # 100

# 无法直接访问私有属性
# print(obj.__secret)      # AttributeError
# print(obj.__another)     # AttributeError

# 但可以通过改写后的名称访问(不推荐)
print(obj._Base__secret)        # 42
print(obj._Derived__another)    # 100

封装与接口设计

良好的封装不仅仅是将属性设为私有,更重要的是设计清晰的公共接口。公共接口应该:

  1. 最小化:只暴露必要的方法
  2. 一致性:相似的方法应有相似的行为模式
  3. 稳定性:公共接口应该在版本升级时保持兼容
  4. 自文档化:方法名应清晰表达其功能
class Stack:
    """栈数据结构"""
    
    def __init__(self):
        self._items = []
    
    def push(self, item):
        """入栈"""
        self._items.append(item)
    
    def pop(self):
        """出栈"""
        if self.is_empty():
            raise IndexError("栈为空")
        return self._items.pop()
    
    def peek(self):
        """查看栈顶元素(不出栈)"""
        if self.is_empty():
            raise IndexError("栈为空")
        return self._items[-1]
    
    def is_empty(self):
        """判断栈是否为空"""
        return len(self._items) == 0
    
    def size(self):
        """获取栈的大小"""
        return len(self._items)
    
    def __len__(self):
        return len(self._items)
    
    def __bool__(self):
        return not self.is_empty()

# 公共接口:push, pop, peek, is_empty, size, __len__, __bool__
# 私有实现:_items
# 外部代码不应该直接访问 _items

使用 slots 限制属性

Python 允许使用 __slots__ 来限制类可以拥有的属性。这可以节省内存,并防止动态添加新属性。

class Point:
    __slots__ = ('_x', '_y')
    
    def __init__(self, x, y):
        self._x = x
        self._y = y
    
    @property
    def x(self):
        return self._x
    
    @property
    def y(self):
        return self._y
    
    def __repr__(self):
        return f"Point({self._x}, {self._y})"

p = Point(1, 2)
print(p)           # Point(1, 2)
print(p.x, p.y)    # 1 2

# 尝试添加新属性会失败
# p.z = 3  # AttributeError: 'Point' object has no attribute 'z'

# 尝试动态添加也会失败
# Point.name = "test"  # 这会影响类,但不影响已有实例

练习题

练习 1:实现银行账户类(完整版)

class Account:
    """银行账户类"""
    
    def __init__(self, account_id, owner_name, initial_balance=0):
        self._account_id = account_id
        self._owner_name = owner_name
        self.__balance = initial_balance  # 私有属性
        self._transactions = []  # 受保护的交易记录
        self._frozen = False      # 账户是否被冻结
    
    @property
    def account_id(self):
        """账号(只读)"""
        return self._account_id
    
    @property
    def owner_name(self):
        """户主姓名"""
        return self._owner_name
    
    @owner_name.setter
    def owner_name(self, value):
        if not value or not value.strip():
            raise ValueError("姓名不能为空")
        self._owner_name = value.strip()
    
    @property
    def balance(self):
        """余额(只读)"""
        return self.__balance
    
    @property
    def frozen(self):
        """账户是否冻结"""
        return self._frozen
    
    def _add_transaction(self, transaction_type, amount, description=""):
        """添加交易记录(内部方法)"""
        self._transactions.append({
            "类型": transaction_type,
            "金额": amount,
            "描述": description
        })
    
    def deposit(self, amount, description=""):
        """存款"""
        if self._frozen:
            raise RuntimeError("账户已被冻结,无法操作")
        if amount <= 0:
            raise ValueError("存款金额必须为正数")
        self.__balance += amount
        self._add_transaction("存款", amount, description)
        return True
    
    def withdraw(self, amount, description=""):
        """取款"""
        if self._frozen:
            raise RuntimeError("账户已被冻结,无法操作")
        if amount <= 0:
            raise ValueError("取款金额必须为正数")
        if amount > self.__balance:
            raise ValueError("余额不足")
        self.__balance -= amount
        self._add_transaction("取款", -amount, description)
        return True
    
    def transfer(self, target_account, amount):
        """转账"""
        if self._frozen:
            raise RuntimeError("账户已被冻结,无法操作")
        if amount <= 0:
            raise ValueError("转账金额必须为正数")
        if amount > self.__balance:
            raise ValueError("余额不足")
        self.__balance -= amount
        target_account.__balance += amount  # 注意:这是直接访问,真实场景需要通过方法
        self._add_transaction("转出", -amount, f"转账至{target_account._account_id}")
        target_account._add_transaction("转入", amount, f"转账自{self._account_id}")
        return True
    
    def freeze(self):
        """冻结账户"""
        self._frozen = True
    
    def unfreeze(self):
        """解冻账户"""
        self._frozen = False
    
    def get_transactions(self):
        """获取交易记录"""
        return self._transactions.copy()
    
    def __repr__(self):
        status = "已冻结" if self._frozen else "正常"
        return f"Account({self._account_id}, {self._owner_name}, 余额={self.__balance}, 状态={status})"

# 测试
acc1 = Account("ACC001", "张三", 10000)
acc2 = Account("ACC002", "李四", 5000)

acc1.deposit(2000, "工资")
acc1.withdraw(500, "购物")
acc1.transfer(acc2, 3000, "借款")

print(acc1)
print(acc2)

print("\n张三的交易记录:")
for t in acc1.get_transactions():
    print(f"  {t}")

print("\n李四的交易记录:")
for t in acc2.get_transactions():
    print(f"  {t}")

练习 2:实现带验证的列表类

class ValidatedList:
    """带验证的列表,只能包含指定类型的元素"""
    
    def __init__(self, item_type):
        self._item_type = item_type
        self._items = []
    
    def __len__(self):
        return len(self._items)
    
    def __getitem__(self, index):
        return self._items[index]
    
    def __iter__(self):
        return iter(self._items)
    
    def __repr__(self):
        return f"ValidatedList({self._item_type.__name__})({self._items!r})"
    
    def __str__(self):
        return str(self._items)
    
    def append(self, item):
        if not isinstance(item, self._item_type):
            raise TypeError(f"期望 {self._item_type.__name__} 类型,得到 {type(item).__name__}")
        self._items.append(item)
    
    def insert(self, index, item):
        if not isinstance(item, self._item_type):
            raise TypeError(f"期望 {self._item_type.__name__} 类型,得到 {type(item).__name__}")
        self._items.insert(index, item)
    
    def pop(self, index=-1):
        return self._items.pop(index)

# 测试
int_list = ValidatedList(int)
int_list.append(1)
int_list.append(2)
int_list.append(3)
print(int_list)  # [1, 2, 3]

try:
    int_list.append("字符串")  # TypeError
except TypeError as e:
    print(f"错误:{e}")

str_list = ValidatedList(str)
str_list.append("hello")
str_list.append("world")
print(str_list)  # ['hello', 'world']

练习 3:实现只读的属性集合

class ReadOnlyDict:
    """只读字典"""
    
    def __init__(self, **kwargs):
        self._data = dict(kwargs)
    
    def __getitem__(self, key):
        return self._data[key]
    
    def __len__(self):
        return len(self._data)
    
    def __iter__(self):
        return iter(self._data)
    
    def __contains__(self, key):
        return key in self._data
    
    def get(self, key, default=None):
        return self._data.get(key, default)
    
    def keys(self):
        return self._data.keys()
    
    def values(self):
        return self._data.values()
    
    def items(self):
        return self._data.items()
    
    def __repr__(self):
        return f"ReadOnlyDict({self._data!r})"
    
    # 禁止修改操作
    def __setitem__(self, key, value):
        raise TypeError("只读字典不支持写操作")
    
    def __delitem__(self, key):
        raise TypeError("只读字典不支持删除操作")
    
    def clear(self):
        raise TypeError("只读字典不支持清空操作")
    
    def pop(self, *args):
        raise TypeError("只读字典不支持 pop 操作")
    
    def update(self, *args, **kwargs):
        raise TypeError("只读字典不支持更新操作")

# 测试
config = ReadOnlyDict(host="localhost", port=3306, debug=True)
print(config)
print(f"host = {config['host']}")
print(f"debug = {config.get('debug')}")
print(f"nonexistent = {config.get('nonexistent', 'default')}")

try:
    config["host"] = "remote"  # TypeError
except TypeError as e:
    print(f"错误:{e}")

总结

今天我们学习了 Python 中封装的概念和实现方式:

  1. 命名约定

    • _name:受保护属性,约定不直接外部访问
    • __name:私有属性,使用名称改写机制
    • name_:避免与 Python 关键字冲突的命名方式
  2. @property 装饰器

    • 创建属性访问器的 Pythonic 方式
    • 可以创建计算属性(只读属性)
    • 可以同时定义 getter、setter、deleter
  3. 封装的好处

    • 保护数据不被意外或恶意修改
    • 在访问和修改属性时执行验证逻辑
    • 提供清晰的公共接口,隐藏内部实现细节
    • 提高代码的可维护性和可扩展性
  4. __slots__

    • 限制类可以拥有的属性
    • 节省内存
    • 防止动态添加新属性

掌握封装是编写高质量面向对象代码的基础。在后续的学习中,我们将学习继承和派生类的概念。