Skip to content

Python 访问限制:给对象的"隐私"加把锁

引言:保险柜与便利贴

假设你有一个日记本,放在客厅桌上。任何人路过都可以翻开看,甚至拿笔涂改——你的秘密毫无保障。

后来你买了保险柜,把日记本锁进去。想看的人必须通过你同意,你用钥匙打开给他们看;想改的人必须告诉你理由,你觉得合理才动手。

Python 的访问限制就是给对象的属性"上锁":有些数据可以公开(像便利贴贴在桌上),有些数据必须保护(像日记本锁进保险柜)。


一、为什么需要访问限制?

1.1 先看问题:属性被随意修改

上一篇我们写的 Student 类:

python
class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score

外部代码可以随便改:

python
>>> bart = Student('Bart Simpson', 59)
>>> bart.score
59
>>> bart.score = 999    # 手滑多打了两个 9
>>> bart.score
999

成绩变成 999 分?显然不合理。但 Python 不会阻止你——它相信你知道自己在做什么。

1.2 问题的本质

数据和操作分离了。score 是 Student 的核心数据,但修改它的权利却暴露在外部——任何人都能改,没有任何检查。

生活化理解:就像你的银行卡余额直接写在卡面上,任何人都能拿笔改掉。银行必须把它锁进系统,修改必须经过验证(密码、身份确认)。


二、私有变量:双下划线 __ 的魔法

2.1 把属性藏起来

在变量名前面加两个下划线 __,它就变成了私有变量(private)

python
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))

改完后,外部代码无法直接访问

python
>>> 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 内部方法照常使用

注意:私有变量只是对外隐藏,类内部的方法完全可以正常访问:

python
>>> bart.print_score()
Bart Simpson: 59

print_score 是类内部的方法,它访问 self.__nameself.__score 毫无障碍。

生活化理解:保险柜里的东西,你自己(类内部)随时能打开看;但客人(外部代码)看不到,除非经过你同意。


三、get/set 方法:受控的访问通道

3.1 只读:提供 get 方法

外部完全不能访问,那要查看怎么办?给类增加 get 方法

python
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

现在外部可以"读",但不能"写":

python
>>> bart.get_name()
'Bart Simpson'
>>> bart.get_score()
59
>>> bart.__name = 'Hack'    # 报错!无法修改

3.2 可写:提供 set 方法

如果确实需要修改,再增加 set 方法

python
def set_score(self, score):
    self.__score = score

3.3 set 方法的核心价值:参数检查

你可能会问:直接 bart.score = 99 也能改,为什么要大费周折写 set_score

因为在 set 方法里可以加检查!

python
def set_score(self, score):
    if 0 <= score <= 100:
        self.__score = score
    else:
        raise ValueError('bad score')

现在:

python
>>> 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:君子协定

python
class Student(object):
    def __init__(self, name):
        self._name = name    # 单下划线

外部可以访问 bart._name,但看到下划线开头,懂的程序员就知道:"这是内部变量,别乱动。"

生活化理解:像冰箱上贴的标签"我的酸奶,别喝"——技术上你能打开冰箱喝掉,但看到标签就该自觉。

4.2 双下划线 __name:真上锁

python
self.__name = name

外部直接访问会报错。Python 真的"上锁"了。

4.3 双下划线开头+结尾 __name__:系统专用

python
__init__
__name__
__doc__

这些是 Python 的特殊变量/方法,系统自己用的。你自己起名时千万别用这种格式,否则可能和系统功能冲突。


五、双下划线的"后门":名称改写

5.1 __name 真的访问不到吗?

其实有个"后门":

python
>>> bart = Student('Bart Simpson', 59)
>>> bart._Student__name
'Bart Simpson'

能访问!为什么?

原理:Python 解释器把 __name 自动改名成了 _Student__name。外部访问 bart.__name 时找不到,但访问 bart._Student__name 就能找到。

5.2 但强烈不建议这么做!

python
# ❌ 别这么干!
bart._Student__name = 'Hack'

原因:

  1. 不同 Python 版本改写规则可能不同,代码会失效;
  2. 破坏封装性,失去了访问限制的意义;
  3. 团队禁忌,看到这种代码会被同事鄙视。

一句话:Python 没有真正的"绝对私有",一切靠自觉。


六、常见误区与避坑指南

6.1 误区一:以为 __name 是"绝对安全"的

如上所述,__name 只是"改名隐藏",不是加密。别把它当成安全机制——它防的是"手滑误改",防不住"恶意攻击"。

6.2 误区二:外部给实例新增 __name 属性,以为覆盖了内部值

python
>>> 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 方法写太多,过度封装

python
# ❌ 每个属性都写 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 个方法

原则只给需要保护的属性加 __。像 addressphone 这种外部本来就要读写的,保持公开即可。过度封装让代码又臭又长。

6.4 误区四:在 __init__ 里就调用 set 方法

python
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'):

python
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('测试成功!')
参考答案
python
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 通过方法计算,不存储
python
c = Circle(5)
print(c.get_radius())   # 5
print(c.area())         # 78.5
c.radius = 10           # 应该无效或报错
参考答案
python
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) 没有报错?

python
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 方法直接改:

python
bart.score = 999    # 没有任何检查,直接成功!

修复__init__ 里也要用 self.__score

python
def __init__(self, name, score):
    self.name = name
    self.__score = score    # 改为私有

八、小结

  1. 双下划线 __ 开头让属性变成私有,外部无法直接访问;
  2. get 方法提供"只读"通道,set 方法提供"受控写入"通道;
  3. set 方法的核心价值是参数检查——防止非法值破坏数据;
  4. 三种下划线含义不同_name 是约定私有,__name 是真私有,__name__ 是系统特殊变量;
  5. __name 不是绝对安全——Python 会把它改写成 _ClassName__name,但别走后门;
  6. 只给需要保护的属性加 __——过度封装让代码臃肿;
  7. 初始化时也调用 set 方法——让非法值在创建阶段就被拦截。

访问限制的本质不是"防贼",而是"防手滑"——它让类的使用者(包括未来的你)不容易犯错,让代码更健壮、更可维护。