Python 类属性封装与访问控制教程
刚接触 Python 面向对象编程(OOP)的朋友,大概率踩过这两个坑:
- 给属性加了
__ 前缀以为“锁死”了,结果换个方式还是能读写;
- 外部随便给实例属性赋值(比如把学生分数改成 -10 或 200),逻辑崩了都找不到源头。
这就是封装没做到位的问题——今天我们就用 Python 的命名约定和工具,把类属性的“权限门”搭得明明白白。
1. 先搞懂:什么是真正的封装?
OOP 三大特性(封装、继承、多态)里,封装是基础。它的核心不是把属性“完全藏起来看不见摸不着”,而是:
- 把「数据」和「操作数据的方法」绑定在一个类里;
- 对外部隐藏内部实现细节(比如存分数用了什么变量名);
- 只暴露安全的、受控制的访问入口(比如只能把分数改成 0-100)。
Python 没有像 Java/C++ 那样的 public/protected/private 关键字,但通过命名约定和语法糖,完全能实现类似的效果。
2. Python 的三种“访问控制级别”
注意引号——这些都是约定俗成的规则,除了双下划线会被编译器做小动作外,Python 本身不会强制拦截任何访问(毕竟 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) # 输出 Alice,但不建议这么写
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
# 分数的 getter
def get_score(self):
return self.__score
# 分数的 setter(加了 0-100 的验证)
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,不加 @xxx.setter
@property
def name(self):
"""学生姓名(只读)"""
return self.__name
# 可读可写属性:先加 @property 定义 getter,再加 @xxx.setter 定义 setter
@property
def score(self):
"""学生分数(0-100)"""
return self.__score
@score.setter # 注意格式:必须和 getter 的方法名完全一致
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 # 输出 90,像赋值公共属性一样
# s.score = -5 # 报错:ValueError: 分数必须在 0-100 之间
4. 揭秘名称改写:为什么“假锁”能被打开?
刚才说双下划线的成员是“假锁”,那怎么打开?直接看 dir() 打印的实例属性列表就知道了——Python 把 __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';
- 后续修改也只能是这两个值;
- 用你喜欢的方式(传统 getter/setter 或 @property)实现。
参考答案(用 @property)
class Student:
def __init__(self, name, gender):
self.__name = name
self.__gender = None # 先设为 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 里加就行)。赶紧去你的项目里试试吧!