封装
约 1897 字大约 6 分钟
2026-01-22
封装(Encapsulation)是面向对象编程(OOP)中的一个重要概念,它通过将数据(属性)和操作数据的方法(行为)绑定在一起,形成一个独立的单元(类),并隐藏类的内部实现细节,只暴露必要的接口给外部使用。
- 基本概念
- 基本语法
- 业务示例
- 注意事项
- 1先把属性都写成公有,等真要做校验/计算时再用 @property —— 别一开始就 getter/setter。
- 2写一个带 @balance.setter 的账户类,setter 里拒绝负值,体会“接口稳定,内部规则集中”。
- 3把私有属性 __password 用 obj._Account__password 强行读出来 —— 体会 Python “君子协定”,private 不是物理隔离。
- 4对子类同名 __x 现象:父类和子类各自的 __x 互不影响(name mangling 防覆盖),跑一遍验证。
- 5不要复制 Java 的 getXxx/setXxx 全套;Python 的封装风格是“以属性形式暴露,必要时才换 property”。
基本概念
封装(Encapsulation)是面向对象编程(OOP)中的一个重要概念,它通过将数据(属性)和操作数据的方法(行为)绑定在一起,形成一个独立的单元(类),并隐藏类的内部实现细节,只暴露必要的接口给外部使用。
在 Python 中,封装主要体现为对属性和方法访问权限的控制。与 Java 或 C++ 不同,Python 并没有严格的 private 关键字来强制禁止访问,而是依靠命名约定和名称改写(Name Mangling)机制。
基本语法
Python 中通过属性名前的下划线数量来区分访问权限:
1. 公有成员 (Public)
- 命名:无下划线开头(例如
self.name)。 - 特点:在类的内部和外部都能随意访问。
- 场景:默认情况。
class Dog:
def __init__(self, name):
self.name = name # 公有属性
dog = Dog('Buddy')
print(dog.name) # 外部可以直接访问 -> Buddy
dog.name = 'Max' # 外部可以直接修改2. 受保护成员 (Protected)
- 命名:单下划线开头(例如
self._age)。 - 特点:约定俗成不想被外部直接访问,但从语法上讲,外部依然可以访问。它主要用于提醒开发者:“这个变量主要是给类内部或子类使用的,请谨慎修改”。
- 场景:内部辅助变量、供子类继承使用的内部状态。
class Dog:
def __init__(self, name, age):
self.name = name
self._age = age # 受保护属性
dog = Dog('Buddy', 3)
print(dog._age) # 可以访问,但 IDE 可能会警告,且不推荐这样做3. 私有成员 (Private)
- 命名:双下划线开头(例如
self.__password)。 - 特点:类外部无法直接通过属性名访问。
- 原理:Python 解释器会将
__attribute改写为_ClassName__attribute(这被称为 Name Mangling),从而防止外部意外访问或子类意外覆盖。
class Account:
def __init__(self, id, password):
self.id = id
self.__password = password # 私有属性
acc = Account(1, '123456')
# print(acc.__password) # 报错: AttributeError: 'Account' object has no attribute '__password'
# 强行访问(不推荐!)
print(acc._Account__password) # 输出: 1234564. 使用 @property 进行更优雅的封装
在 Python 中,我们通常不建议写类似 Java 的 get_name() 和 set_name() 方法,而是使用 @property 装饰器,像访问属性一样调用方法。
class Student:
def __init__(self, name, score):
self.name = name
self._score = score
@property
def score(self):
"""Getter: 获取分数"""
return self._score
@score.setter
def score(self, value):
"""Setter: 设置分数,并增加数据校验逻辑"""
if not isinstance(value, (int, float)):
raise ValueError('分数必须是数字')
if not 0 <= value <= 100:
raise ValueError('分数必须在 0 到 100 之间')
self._score = value
s = Student('Alice', 90)
print(s.score) # 自动调用 getter -> 90
s.score = 95 # 自动调用 setter
print(s.score) # -> 95
# s.score = 105 # 会抛出 ValueError: 分数必须在 0 到 100 之间业务示例
假设我们需要设计一个银行账户(BankAccount)类。核心需求是:余额(balance)不能随意修改,必须通过存款(deposit)或取款(withdraw)方法来变动,且变动需要记录日志,同时余额不能为负数。
如果直接公开 balance 属性,外部代码可以直接 acc.balance = -100,这会导致严重的数据一致性问题。
优化后的实现
import datetime
class BankAccount:
def __init__(self, owner, initial_balance=0):
self.owner = owner
self.__balance = initial_balance # 使用私有属性,禁止外部直接修改
self._transactions = [] # 交易记录
@property
def balance(self):
"""只读属性,只允许获取,不允许直接赋值"""
return self.__balance
def deposit(self, amount):
if amount <= 0:
raise ValueError('存款金额必须大于 0')
self.__balance += amount
self._log_transaction(f'存款: +{amount}')
print(f'存款成功,当前余额: {self.__balance}')
def withdraw(self, amount):
if amount <= 0:
raise ValueError('取款金额必须大于 0')
if amount > self.__balance:
raise ValueError('余额不足')
self.__balance -= amount
self._log_transaction(f'取款: -{amount}')
print(f'取款成功,当前余额: {self.__balance}')
def _log_transaction(self, note):
"""内部方法,记录交易日志"""
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self._transactions.append(f'[{timestamp}] {note}')
def print_statement(self):
print(f'--- {self.owner} 的交易流水 ---')
for log in self._transactions:
print(log)
print(f'最终余额: {self.__balance}')
# --- 客户端调用 ---
my_acc = BankAccount('Bob', 100)
# my_acc.balance = 200 # 报错: AttributeError (或者如果定义了 setter 但没写逻辑,IDE会提示只读)
# my_acc.__balance = 200 # 报错: 无法访问
print(f'初始余额: {my_acc.balance}') # 访问 getter
my_acc.deposit(50)
my_acc.withdraw(30)
# my_acc.withdraw(200) # 报错: ValueError: 余额不足
my_acc.print_statement()这种设计体现了封装的价值:
- 数据保护:
balance无法被外部随意篡改。 - 逻辑收敛:存款和取款的逻辑(如负数检查、余额校验、日志记录)都被封装在方法内部,外部调用者不需要关心这些细节。
- 接口稳定:外部只需要知道调用
deposit和withdraw,即使内部实现(例如日志记录方式)变了,外部代码也不需要修改。
注意事项
1. "We are all consenting adults"
这是 Python 社区的一句名言。Python 不像 Java 那样通过编译器强制执行权限控制(如 private 真的无法访问)。Python 选择相信开发者:既然我通过下划线告诉了你这是内部变量,如果你非要通过 _ClassName__attr 去访问它,那你应该知道自己在做什么,并承担后果。
2. 不要滥用 getter/setter
在 Java 中,习惯把所有属性设为 private,然后生成 getter/setter。但在 Python 中,如无必要,勿增实体(Property)。
- 初始阶段:直接使用公有属性(
public)。 - 演进阶段:如果你后来发现需要对某个属性赋值进行校验,再将其改为
@property。 - 优势:由于客户端代码也是通过
obj.attr访问的,所以从公有属性切换到@property不会破坏现有的客户端代码(无需把obj.attr改为obj.get_attr())。
3. 名称改写(Name Mangling)的真正目的
双下划线 __var 的主要目的并不是为了“安全”或“隐私”,而是为了防止子类在继承时不小心覆盖了父类的同名内部变量。
class Parent:
def __init__(self):
self.__secret = 'Parent\'s secret'
class Child(Parent):
def __init__(self):
super().__init__()
self.__secret = 'Child\'s secret' # 不会覆盖父类的 __secret
p = Parent()
c = Child()
# 此时 c 实例中实际上同时存在 _Parent__secret 和 _Child__secret- 封装 = 把数据和操作绑到一起,对外只暴露受控接口;目的是“别人改不坏内部状态”。
- Python 用约定而非编译器:_x 表示内部,__x 触发 name mangling,没有真 private。
- 起步写公有属性即可;后来需要校验/计算时换 @property,不破坏现有调用方。
- name mangling 的真目的是“防止子类不小心覆盖父类同名变量”,而不是“安全隐藏”。
- 封装好坏看接口稳定:外面只调 deposit/withdraw,内部加日志、限额都不用改调用方。
版权所有
版权归属:Shuo Liu
