Python 类属性封装与访问控制教程
刚接触 Python oop(OOP)的朋友,大概率踩过这两个坑:
- 给属性加了
__ 前缀以为“锁死”了,结果换个方式还是能读写;
- 外部随便给实例属性赋值(比如把学生分数改成
-10 或 200),逻辑崩了都找不到源头。
这就是封装没做到位的问题——今天我们就用 Python 的命名约定和工具,把类属性的访问控制搭得明明白白。
1. 先搞懂:什么是真正的封装?
OOP 三大特性(封装、继承、多态)里,封装是基础。它的核心不是把属性完全藏起来,而是:
- 把数据和操作数据的方法绑定在一个类里;
- 对外部隐藏内部实现细节(比如存分数用了什么变量名);
- 只暴露安全的、受控制的访问入口(比如只能把分数改成 0-100)。
Python 没有像 Java / C++ 那样的 public、protected、private 关键字,但通过命名约定和语法糖,完全能实现类似的效果。
2. Python 的三种“访问控制级别”
TIP
以下都是约定俗成的规则。除了双下划线会被编译器做小动作外,Python 本身不会强制拦截任何访问——毕竟这是一门“我们都是成年人”的语言。
2.1 公共成员(Public)
默认所有属性/方法都是公共的,类内部、外部、子类都能随便用。
class Student:
def __init__(self, name):
self.name = name # 公共属性:完全暴露
s = Student("Alice")
print(s.name) # Alice
s.name = "Bob" # 直接修改也无妨
2.2 受保护成员(Protected)
以单个下划线 _ 开头的成员,按约定属于给自己和子类用的内部成员,外部最好别碰。
但 Python 不会拦你——硬要访问代码照样跑,只是团队其他人会觉得你“不懂事”。
class Student:
def __init__(self, name):
self._name = name # “请勿打扰”的内部属性
s = Student("Alice")
print(s._name) # 虽然能跑,但强烈不建议
2.3 私有成员(Private)
以双下划线 __ 开头、非双下划线结尾的成员,Python 编译器会自动做名称改写(Name Mangling),变成 _ClassName__var 的格式。表面上看外部无法直接访问。
class Student:
def __init__(self, name):
self.__name = name # 私有属性:上了把“假锁”
s = Student("Alice")
# print(s.__name) # AttributeError: 'Student' object has no attribute '__name'
3. 假锁变真门:实现受控制的封装
虽然双下划线防不住有心人,但配合访问器(getter / setter),就能变成真正的“安全门”——我们可以在门里加上参数验证、日志记录、权限检查等逻辑。
3.1 传统方法:显式写 getter / setter
早期 Python 或习惯 Java / C++ 的开发者会这么写:
class Student:
def __init__(self, name, score):
self.__name = name # 姓名一般入学后不修改,设为私有只读
self.__score = score # 分数可读写,但需要验证
# 只读:只提供 getter,不提供 setter
def get_name(self):
return self.__name
def get_score(self):
return self.__score
def set_score(self, score):
if not isinstance(score, (int, float)):
raise TypeError("分数必须是数字")
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError("分数必须在 0-100 之间")
不过这种写法不够 Pythonic——调用时得写 s.get_name(),而不是更自然的 s.name。
3.2 推荐方法:使用 @property 装饰器
Python 提供了 @property 这个语法糖,能把方法“伪装”成属性,既保留安全验证,又拥有公共属性般的简洁调用。
class Student:
def __init__(self, name, score):
self.__name = name
self.__score = score
@property
def name(self):
"""学生姓名(只读)"""
return self.__name
@property
def score(self):
"""学生分数(0-100)"""
return self.__score
@score.setter
def score(self, value):
if not isinstance(value, (int, float)):
raise TypeError("分数必须是数字")
if 0 <= value <= 100:
self.__score = value
else:
raise ValueError("分数必须在 0-100 之间")
现在外部调用非常自然:
s = Student("Bob", 85)
print(s.name) # Bob
# s.name = "Charlie" # 报错:AttributeError: can't set attribute(没有 setter)
s.score = 90 # 像操作公共属性一样
# s.score = -5 # 报错:ValueError: 分数必须在 0-100 之间
4. 揭秘名称改写:为什么“假锁”能被打开?
双下划线的成员只是名称被改写了,并非真正隐藏。用 dir() 查看实例属性就能发现端倪:__name 变成了 _Student__name。
class Student:
def __init__(self, name):
self.__name = name
s = Student("Alice")
print(dir(s)) # 能看到 '_Student__name'
print(s._Student__name) # 输出 Alice!但生产代码里千万别这么干
WARNING
- 违反封装的约定;
- 类名一旦改变,改写名称也会跟着变,代码立刻崩;
- 其他开发者看到会直接血压飙升。
5. 别搞混:特殊变量名
还有一种以双下划线开头、双下划线结尾的变量/方法,比如 __init__、__len__、__dict__。这是 Python 的魔术方法/属性,不会被名称改写,可以直接访问。
class Student:
def __init__(self):
self.__special_attr__ = "我是特殊属性,不会被改写"
s = Student()
print(s.__special_attr__) # 正常输出
6. 动手练:封装 gender 属性
来个小练习,要求:
- 把
gender 设为私有属性;
- 只能初始化为
'male' 或 'female';
- 后续修改也只能是这两个值;
- 用
@property 实现。
参考实现
class Student:
def __init__(self, name, gender):
self.__name = name
self.__gender = None # 先占位,靠 setter 做统一校验
self.gender = gender # 走下面的 setter
@property
def gender(self):
return self.__gender
@gender.setter
def gender(self, value):
if value not in ('male', 'female'):
raise ValueError("性别必须是 'male' 或 'female'")
self.__gender = value
# 测试代码
if __name__ == "__main__":
try:
bart = Student('Bart', 'male')
assert bart.gender == 'male'
bart.gender = 'female'
assert bart.gender == 'female'
# bart.gender = 'unknown' # 应该报错
print("✅ 测试通过!")
except Exception as e:
print(f"❌ 测试失败:{e}")
7. 最佳实践总结
- 默认设私有,按需暴露:除非确定属性“完全公开、无需验证”,否则都加
__ 前缀;
- 用
@property 代替显式 getter / setter:更 Pythonic,调用更自然;
- setter 必须加验证:这是封装的核心价值之一,别只做简单赋值;
- 单下划线仅用于内部约定:比如工具方法、暂时不想公开的属性;
- 不要碰名称改写后的变量:碰了就是给自己和团队挖坑;
- 合理设计只读 / 读写属性:姓名、身份证号这类属性,一般只留
@property,不加 @xxx.setter。
好的封装能让代码更健壮(杜绝外部误操作)、更易维护(内部实现改了,外部调用无感)、更易扩展(以后加权限检查、日志记录,直接在 setter 里改就行)。赶紧去你的项目里试试吧!