Python 访问限制:给对象的"隐私"加把锁
引言:保险柜与便利贴
假设你有一个日记本,放在客厅桌上。任何人路过都可以翻开看,甚至拿笔涂改——你的秘密毫无保障。
后来你买了保险柜,把日记本锁进去。想看的人必须通过你同意,你用钥匙打开给他们看;想改的人必须告诉你理由,你觉得合理才动手。
Python 的访问限制就是给对象的属性"上锁":有些数据可以公开(像便利贴贴在桌上),有些数据必须保护(像日记本锁进保险柜)。
一、为什么需要访问限制?
1.1 先看问题:属性被随意修改
上一篇我们写的 Student 类:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score外部代码可以随便改:
>>> bart = Student('Bart Simpson', 59)
>>> bart.score
59
>>> bart.score = 999 # 手滑多打了两个 9
>>> bart.score
999成绩变成 999 分?显然不合理。但 Python 不会阻止你——它相信你知道自己在做什么。
1.2 问题的本质
数据和操作分离了。score 是 Student 的核心数据,但修改它的权利却暴露在外部——任何人都能改,没有任何检查。
生活化理解:就像你的银行卡余额直接写在卡面上,任何人都能拿笔改掉。银行必须把它锁进系统,修改必须经过验证(密码、身份确认)。
二、私有变量:双下划线 __ 的魔法
2.1 把属性藏起来
在变量名前面加两个下划线 __,它就变成了私有变量(private):
class Student(object):
def __init__(self, name, score):
self.__name = name # 私有
self.__score = score # 私有
def print_score(self):
print('%s: %s' % (self.__name, self.__score))改完后,外部代码无法直接访问:
>>> bart = Student('Bart Simpson', 59)
>>> bart.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'报错!外部再也摸不到 __name 和 __score 了。
2.2 内部方法照常使用
注意:私有变量只是对外隐藏,类内部的方法完全可以正常访问:
>>> bart.print_score()
Bart Simpson: 59print_score 是类内部的方法,它访问 self.__name 和 self.__score 毫无障碍。
生活化理解:保险柜里的东西,你自己(类内部)随时能打开看;但客人(外部代码)看不到,除非经过你同意。
三、get/set 方法:受控的访问通道
3.1 只读:提供 get 方法
外部完全不能访问,那要查看怎么办?给类增加 get 方法:
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def get_name(self):
return self.__name
def get_score(self):
return self.__score现在外部可以"读",但不能"写":
>>> bart.get_name()
'Bart Simpson'
>>> bart.get_score()
59
>>> bart.__name = 'Hack' # 报错!无法修改3.2 可写:提供 set 方法
如果确实需要修改,再增加 set 方法:
def set_score(self, score):
self.__score = score3.3 set 方法的核心价值:参数检查
你可能会问:直接 bart.score = 99 也能改,为什么要大费周折写 set_score?
因为在 set 方法里可以加检查!
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')现在:
>>> bart.set_score(99) # 正常
>>> bart.set_score(999) # 报错:ValueError: bad score生活化理解:保险柜加了密码锁——你想改里面的东西,得先输对密码(参数检查),输错了门都不开。
| 方式 | 能否检查 | 安全性 |
|---|---|---|
直接 bart.score = 99 | ❌ 不能 | 低,随意改 |
bart.set_score(99) | ✅ 能 | 高,受控 |
四、三种下划线命名的区别
Python 中下划线开头有特殊含义,但双下划线开头 ≠ 双下划线开头+结尾 ≠ 单下划线开头,别搞混:
| 命名风格 | 例子 | 含义 | 外部能否访问 |
|---|---|---|---|
| 普通名字 | name | 公开属性 | ✅ 可以 |
| 单下划线开头 | _name | 约定私有:"请别碰我" | ⚠️ 技术上能,习惯上别 |
| 双下划线开头 | __name | 真正私有:外部无法直接访问 | ❌ 报错 |
| 双下划线开头+结尾 | __name__ | 特殊变量:Python 系统用 | ✅ 可以,但别自己起这种名 |
4.1 单下划线 _name:君子协定
class Student(object):
def __init__(self, name):
self._name = name # 单下划线外部可以访问 bart._name,但看到下划线开头,懂的程序员就知道:"这是内部变量,别乱动。"
生活化理解:像冰箱上贴的标签"我的酸奶,别喝"——技术上你能打开冰箱喝掉,但看到标签就该自觉。
4.2 双下划线 __name:真上锁
self.__name = name外部直接访问会报错。Python 真的"上锁"了。
4.3 双下划线开头+结尾 __name__:系统专用
__init__
__name__
__doc__这些是 Python 的特殊变量/方法,系统自己用的。你自己起名时千万别用这种格式,否则可能和系统功能冲突。
五、双下划线的"后门":名称改写
5.1 __name 真的访问不到吗?
其实有个"后门":
>>> bart = Student('Bart Simpson', 59)
>>> bart._Student__name
'Bart Simpson'能访问!为什么?
原理:Python 解释器把 __name 自动改名成了 _Student__name。外部访问 bart.__name 时找不到,但访问 bart._Student__name 就能找到。
5.2 但强烈不建议这么做!
# ❌ 别这么干!
bart._Student__name = 'Hack'原因:
- 不同 Python 版本改写规则可能不同,代码会失效;
- 破坏封装性,失去了访问限制的意义;
- 团队禁忌,看到这种代码会被同事鄙视。
一句话:Python 没有真正的"绝对私有",一切靠自觉。
六、常见误区与避坑指南
6.1 误区一:以为 __name 是"绝对安全"的
如上所述,__name 只是"改名隐藏",不是加密。别把它当成安全机制——它防的是"手滑误改",防不住"恶意攻击"。
6.2 误区二:外部给实例新增 __name 属性,以为覆盖了内部值
>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 看起来设置了 __name
>>> bart.__name
'New Name'
>>> bart.get_name() # 但 get_name 返回的还是旧的!
'Bart Simpson'为什么?
- 外部
bart.__name = 'New Name'创建了一个新属性__name(没被改写); - 内部的
self.__name实际是self._Student__name; - 两者根本不是同一个变量!
生活化理解:你在便利贴上写了"New Name"贴在保险柜外面,保险柜里的东西根本没动。
6.3 误区三:get/set 方法写太多,过度封装
# ❌ 每个属性都写 get/set,代码臃肿
class Student(object):
def __init__(self, name, score, age, gender, address, phone):
self.__name = name
self.__score = score
self.__age = age
self.__gender = gender
self.__address = address
self.__phone = phone
def get_name(self): return self.__name
def set_name(self, v): self.__name = v
def get_score(self): return self.__score
def set_score(self, v): self.__score = v
def get_age(self): return self.__age
def set_age(self, v): self.__age = v
# ... 还有 8 个方法原则:只给需要保护的属性加 __。像 address、phone 这种外部本来就要读写的,保持公开即可。过度封装让代码又臭又长。
6.4 误区四:在 __init__ 里就调用 set 方法
class Student(object):
def __init__(self, name, score):
self.__name = name
self.set_score(score) # ✅ 好习惯:初始化也走检查
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')这样即使创建实例时传入非法分数,也能被拦截。
七、实战练习
练习 1:隐藏 gender 字段
把下面 Student 类的 gender 字段隐藏起来,用 get_gender() 和 set_gender() 代替,并检查参数有效性(只能是 'male' 或 'female'):
class Student(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
# 测试:
bart = Student('Bart', 'male')
if bart.get_gender() != 'male':
print('测试失败!')
else:
bart.set_gender('female')
if bart.get_gender() != 'female':
print('测试失败!')
else:
print('测试成功!')参考答案
class Student(object):
def __init__(self, name, gender):
self.name = name
self.set_gender(gender) # 初始化也走检查
def get_gender(self):
return self.__gender
def set_gender(self, gender):
if gender in ('male', 'female'):
self.__gender = gender
else:
raise ValueError('bad gender')练习 2:只读属性
设计一个 Circle 类:
- 半径
radius公开可读,但创建后不可修改(不提供 set 方法) - 面积
area通过方法计算,不存储
c = Circle(5)
print(c.get_radius()) # 5
print(c.area()) # 78.5
c.radius = 10 # 应该无效或报错参考答案
class Circle(object):
def __init__(self, radius):
self.__radius = radius
def get_radius(self):
return self.__radius
def area(self):
return 3.14 * self.__radius ** 2
c = Circle(5)
print(c.get_radius()) # 5
print(c.area()) # 78.5
c.radius = 10 # 只是新增了一个公开属性 radius,不影响 __radius
print(c.get_radius()) # 还是 5练习 3:找出 bug
下面代码为什么 bart.set_score(999) 没有报错?
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def set_score(self, score):
if 0 <= score <= 100:
self.score = score
else:
raise ValueError('bad score')
bart = Student('Bart', 59)
bart.set_score(999)
print(bart.score)参考答案
__init__ 里用的是 self.score = score(公开属性),而不是 self.__score = score(私有属性)。
虽然 set_score 方法里检查了 0 <= score <= 100,但外部代码完全可以绕过 set 方法直接改:
bart.score = 999 # 没有任何检查,直接成功!修复:__init__ 里也要用 self.__score:
def __init__(self, name, score):
self.name = name
self.__score = score # 改为私有八、小结
- 双下划线
__开头让属性变成私有,外部无法直接访问; - get 方法提供"只读"通道,set 方法提供"受控写入"通道;
- set 方法的核心价值是参数检查——防止非法值破坏数据;
- 三种下划线含义不同:
_name是约定私有,__name是真私有,__name__是系统特殊变量; __name不是绝对安全——Python 会把它改写成_ClassName__name,但别走后门;- 只给需要保护的属性加
__——过度封装让代码臃肿; - 初始化时也调用 set 方法——让非法值在创建阶段就被拦截。
访问限制的本质不是"防贼",而是"防手滑"——它让类的使用者(包括未来的你)不容易犯错,让代码更健壮、更可维护。