封装与访问控制
本教程共 70 篇 · 第 32 篇 · 更新于 2026-07-22 · 约 5 分钟阅读
32. 封装与访问控制
本节目标:理解封装的意义,掌握单下划线
_var和双下划线__var的区别,学会用property把方法变属性。
上一章我们给实例绑属性,外部随便读写。这在小型脚本里没问题,但项目大了,你很难保证别人不会把数据改得一塌糊涂。
封装(Encapsulation)的核心思想是:把内部数据保护起来,只暴露安全的访问方式。就像你开车,方向盘和油门是暴露的,但发动机舱盖着,你不会直接拨弄火花塞。
公开属性的问题
看这段代码:
class BankAccount:
def __init__(self, balance):
self.balance = balance
acc = BankAccount(1000)
acc.balance = -500 # 余额居然能变负数?
print(acc.balance) # -500
balance 谁都能改,没有任何校验。这在真实系统里是灾难。
单下划线:约定俗成的「内部属性」
Python 没有真正的私有(private)关键字。社区约定:名字前加单下划线 _xxx 的属性,表示「内部使用,外部最好别碰」。
class Person:
def __init__(self, name):
self.name = name
self._age = 0 # 约定:内部属性
p = Person('老王')
print(p._age) # 可以访问,但不建议
单下划线只是提醒,不会阻止访问。它更像一个「此处施工」的告示牌。
双下划线:名字改写(name mangling)
如果属性名前加双下划线 __xxx,Python 会做一件特殊的事:把名字改成 _类名__xxx。这样从外部直接访问会报错。
class BankAccount:
def __init__(self, balance):
self.__balance = balance
def deposit(self, amount):
if amount <= 0:
print("存入金额必须大于零")
return
self.__balance += amount
def get_balance(self):
return self.__balance
acc = BankAccount(1000)
# print(acc.__balance) # AttributeError
acc.deposit(500)
print(acc.get_balance()) # 1500
Note双下划线不是绝对安全。真有执念的人可以通过
acc._BankAccount__balance访问到。它只是防止「无意间」的误操作,不是防黑客。
为什么需要 getter / setter
上面我们用 get_balance() 方法读取余额。这很安全,但写起来啰嗦:
print(acc.get_balance()) # 不如 acc.balance 直观
能不能既保持安全性,又像访问属性一样简洁?
@property:把方法变成属性
Python 提供了 property 装饰器(decorator),让你可以像读属性一样读方法,像写属性一样写方法:
class BankAccount:
def __init__(self, balance):
self.__balance = balance
@property
def balance(self):
return self.__balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("余额不能为负数")
self.__balance = value
acc = BankAccount(1000)
print(acc.balance) # 像读属性一样读方法,输出 1000
acc.balance = 1500 # 像写属性一样调用 setter
print(acc.balance) # 1500
# acc.balance = -100 # 会抛出 ValueError
拆解一下:
@property把balance()变成「只读属性」。@balance.setter给这个属性加上写入逻辑。- 实际数据存在
__balance里,外部不可见。
Tip如果你只想暴露读取,不写
@balance.setter,那这个属性就是只读的。试图赋值会报AttributeError。
只读属性的常见用法
有些属性是计算出来的,不需要存储。比如根据生日算年龄:
from datetime import datetime
class Employee:
def __init__(self, name, birth_year):
self.name = name
self.__birth_year = birth_year
@property
def age(self):
return datetime.now().year - self.__birth_year
e = Employee('小陈', 1995)
print(e.age) # 输出当前年龄
# e.age = 30 # AttributeError: can't set attribute
age 没有对应的 setter,所以是只读的。这很合理:你不能直接「设置年龄」,只能改生日。
什么时候用哪种访问控制
| 写法 | 含义 | 使用场景 |
|---|---|---|
name | 公开属性 | 简单数据,不需要校验 |
_name | 内部属性(约定) | 不想外部直接依赖,但继承子类可能需要 |
__name | 私有属性(名字改写) | 需要严格保护,防止外部和子类误改 |
@property | 属性包装 | 读取/写入时需要额外逻辑(校验、计算、日志) |
Warning不要滥用双下划线。如果你确定子类也需要访问这个属性,用单下划线更合适。双下划线的名字改写会让继承变得麻烦。
一个完整的封装例子
把上面的知识串起来:
class Product:
def __init__(self, name, price, stock):
self.name = name
self.__price = price
self.__stock = stock
@property
def price(self):
return self.__price
@price.setter
def price(self, value):
if value <= 0:
raise ValueError("价格必须大于零")
self.__price = value
@property
def stock(self):
return self.__stock
def sell(self, quantity):
if quantity > self.__stock:
raise ValueError("库存不足")
self.__stock -= quantity
p = Product('鼠标', 99, 100)
p.price = 89
p.sell(5)
print(p.stock) # 95
name 是公开属性,简单直接。price 和 stock 受 property 保护,修改时有校验。__price 和 __stock 是实际存储的私有属性。
小结
- 封装让数据和行为绑定,隐藏内部细节。
_var是约定上的内部属性,仍可访问。__var触发名字改写,防止无意访问。@property让你在保持安全的同时,像操作属性一样读写数据。
封装不是「藏起来不让人看」,而是「提供安全的通道」。就像 ATM 机,你不会直接打开金库数钱,但插卡输密码就能安全取现。
来源:参考了 runoob「Python3 面向对象」、liaoxuefeng「访问限制」和「使用@property」、pythondoc「9. 类」等,改写后所得。