Day 24 - 私有属性与封装
什么是封装?
封装(Encapsulation)是面向对象编程的三大基本特性之一(另外两个是继承和多态)。封装的核心理念是将数据(属性)和操作数据的方法(行为)打包在一起,对外隐藏内部实现细节,只暴露必要的接口。这样做的好处是:提高代码的可维护性、降低模块间的耦合度、保护数据不被意外或恶意修改。
在 Python 中,封装并不是像 Java 或 C++ 那样通过严格的访问控制关键字(public、private、protected)来实现的。Python 采用了一种"约定优于配置"的方式,通过命名规范来标识成员的访问级别。
Python 的命名规范
Python 中以下划线的前缀和数量来表示属性的访问级别:
-
_name(单下划线):受保护的属性,按照约定,外部代码不应该直接访问,但 Python 不会阻止这种访问。这是一种"软私有"的约定。 -
__name(双下划线开头):名称改写(Name Mangling)机制,会将属性名改写为_类名__name的形式。这提供了一定程度的"硬私有"保护,但通过改写后的名称仍然可以访问。 -
__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 类,余额是一个私有属性。如果允许外部代码直接访问和修改余额,那么可能会出现以下问题:
- 数据不一致:余额可能被修改成负数
- 业务逻辑被绕过:取款时没有检查余额是否充足
- 无法追踪变更:直接修改余额时无法记录交易历史
通过将余额设为私有属性,并提供 deposit、withdraw、get_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 确实很有用,例如:
- 在获取或设置属性时执行额外的逻辑
- 提供只读或只写的属性
- 实现计算属性(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
封装与接口设计
良好的封装不仅仅是将属性设为私有,更重要的是设计清晰的公共接口。公共接口应该:
- 最小化:只暴露必要的方法
- 一致性:相似的方法应有相似的行为模式
- 稳定性:公共接口应该在版本升级时保持兼容
- 自文档化:方法名应清晰表达其功能
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 中封装的概念和实现方式:
-
命名约定:
_name:受保护属性,约定不直接外部访问__name:私有属性,使用名称改写机制name_:避免与 Python 关键字冲突的命名方式
-
@property 装饰器:
- 创建属性访问器的 Pythonic 方式
- 可以创建计算属性(只读属性)
- 可以同时定义 getter、setter、deleter
-
封装的好处:
- 保护数据不被意外或恶意修改
- 在访问和修改属性时执行验证逻辑
- 提供清晰的公共接口,隐藏内部实现细节
- 提高代码的可维护性和可扩展性
-
__slots__:- 限制类可以拥有的属性
- 节省内存
- 防止动态添加新属性
掌握封装是编写高质量面向对象代码的基础。在后续的学习中,我们将学习继承和派生类的概念。