在Python里并没有像Java或C++那样严格的私有成员访问控制,开发者常通过命名约定来暗示某些属性不应被外部直接修改。双下划线开头的变量会触发解释器的名称改写,但这层机制的设计初衷并非安全隔离,而是为了防止子类中的命名冲突。理解它的实际行为和局限,对写出可维护且符合预期的代码很重要。

双下划线是如何触发名称改写的
当一个类的属性名以两个下划线开头且不超过一个下划线结尾时,Python会在编译阶段将该名字重写成“_类名+原属性名”的形式。这个过程叫做名称改写(name mangling)。例如类中定义了__count,在类外部实际存储的字段名会变成_MyClass__count。这种改写发生在类体定义时,而不是运行时动态计算,所以通过类的__dict__可以看到改写后的结果。
需要注意,改写只针对类内部出现的双下划线标识符,且只改写那些文本上位于类作用域中的名字。如果是在类方法里通过局部变量或动态字符串拼接去访问,解释器不会自动帮你改写。下面这段代码展示了改写前后的名称差异:
class User:
def __init__(self):
self.__token = "abc123"
def show(self):
# 类内部直接访问,会被改写成 self._User__token
print(self.__token)
u = User()
u.show()
# 外部不能直接用 u.__token 访问
# print(u.__token) # AttributeError
print(u._User__token) # 输出 abc123
print(u.__dict__) # 键为 '_User__token'
从输出可以看出,所谓私有只是名字变了,数据依旧挂在实例上。任何知道改写规则的人都能用单下划线加类名的方式读取。对反射、序列化框架或者调试器来说,这层改名完全没有隐蔽作用。
为什么双下划线不能保证安全访问
名称改写的首要目标是避免继承体系里的意外覆盖。假设父类有__id,子类也定义__id,改写后分别变成_Parent__id和_Child__id,两者互不干扰。这是面向大型代码库和团队协作的防护,而不是给恶意调用者设置的壁垒。如果把它当成安全机制,就会低估外部访问的能力。
在真实项目中,可以通过内省轻松突破限制。比如用getattr(obj, "_ClassName__field")或者遍历obj.__dict__拿到所有改写后的键。甚至使用dir(obj)也能列出包含双下划线改名的成员。下面的示例演示了如何在不修改类源码的情况下读取“私有”数据:
class Wallet:
def __init__(self):
self.__balance = 100
w = Wallet()
# 通过 dir 发现改写后的属性
hidden = [k for k in dir(w) if k.startswith("_Wallet")]
print(hidden) # ['_Wallet__balance']
print(getattr(w, hidden[0])) # 100
这说明双下划线在运行期没有任何访问控制检查。如果真有敏感信息,比如密钥或凭证,应该放在进程外存储、使用操作系统权限隔离,或者至少用加密后再存入对象。把希望寄托在变量名被改写上,属于典型的误解。
更合理的封装替代方案
对于大多数业务代码,单下划线约定已经足够。以单下划线开头的属性(如_internal)向使用者传达“这是内部实现,请勿依赖”的信号,同时不触发名称改写,调试和测试时更友好。Python社区普遍接受这种约定优于强制限制的哲学。
如果希望控制读写逻辑,应该使用property描述符或者@property装饰器。这样可以在赋值和获取时加入校验、日志或懒加载,而不是靠名字混淆。示例如下:
class Account:
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
a = Account(50)
a.balance = 30
print(a.balance)
# a.balance = -1 # 抛异常
当确实需要防止子类覆盖,才考虑双下划线。但务必记住它只是命名层面的提示,不是权限系统。团队内部通过代码规范和审查来保障封装,比依赖语言层面的改名更可靠。
常见误区与小结
不少人以为双下划线能让变量彻底私有,甚至在面试中把它等同于其他语言的private关键字,这是概念厘清上的偏差。Python的设计哲学是“我们都是成年人”,即相信使用者会遵守约定。改写机制解决的是命名空间冲突,不是数据保护。
总结来说,定义Python私有变量有三种层次:单下划线约定用于内部提示,双下划线用于避免继承冲突但不能防访问,property用于真正控制读写行为。根据场景选择,不要神话名称改写的安全能力。