私有变量
Python 没有 private、protected 等访问控制关键字,也不存在从对象外部绝对无法访问的"私有"实例变量。然而,通过命名约定和名称改写(name mangling)机制,Python 提供了两种层次的封装提示。
单下划线约定:内部使用
以单下划线开头的名称(如 _spam)被约定为非公有(non-public)部分。这适用于方法、函数和数据成员,表示它们是实现细节,可能在不通知的情况下改变。外部代码不应直接依赖它们。
class Buffer:
def __init__(self, size):
self._size = size # 内部状态,不建议外部直接修改
self._data = bytearray(size)
def _resize(self, new_size): # 内部辅助方法
self._data = bytearray(new_size)
self._size = new_size
def write(self, chunk):
if len(chunk) > self._size:
self._resize(len(chunk))
self._data[:len(chunk)] = chunk
buf = Buffer(16)
buf.write(b'hello')
# buf._resize(8) # 语法上允许,但违背约定
单下划线前缀纯粹是约定,解释器不会施加任何限制。from module import * 默认不会导入以单下划线开头的名称,但显式导入(如 from module import _spam)仍然可以访问。
双下划线与名称改写
以双下划线开头、最多一个下划线结尾的标识符(如 __spam)会触发名称改写(name mangling)。解释器在类定义内部将这类标识符替换为 _ClassName__spam 的形式,其中 ClassName 是去除前缀下划线后的当前类名。
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # 原始 update 的私有副本
class MappingSubclass(Mapping):
def update(self, keys, values):
# 提供了新的签名,但不会破坏 __init__
for item in zip(keys, values):
self.items_list.append(item)
m = MappingSubclass([1, 2, 3])
print(m.items_list) # [1, 2, 3]
在这个例子中,Mapping.__init__ 中的 self.__update 被改写为 self._Mapping__update。即使 MappingSubclass 也定义了 __update,它会被改写为 _MappingSubclass__update,两者不会冲突。因此 Mapping.__init__ 调用的始终是 Mapping 版本的 update,不受子类重写的影响。
名称改写在类定义内部无条件发生,与标识符的句法位置无关:
class Demo:
__x = 10 # 改写为 _Demo__x
def method(self):
print(self.__x) # 改写为 self._Demo__x
def __private(self): # 改写为 _Demo__private
return 'hidden'
d = Demo()
d.method() # 10
# d.__x # AttributeError
# d.__private() # AttributeError
名称改写的规则细节
改写的具体规则是:对于形式为 __identifier 的名称,替换为 _ClassName__identifier。ClassName 是发生改写的那个类的名称,去除前缀下划线。
class A:
def __init__(self):
self.__value = 1
class B(A):
def __init__(self):
super().__init__()
self.__value = 2 # 改写为 _B__value,与 A 的 _A__value 不冲突
b = B()
print(b._A__value) # 1
print(b._B__value) # 2
注意:名称改写不考虑继承层次。在 B 的方法中写 self.__value,总是改写为 _B__value,即使 A 中也有 __value。这是有意设计的,目的是防止子类意外覆盖父类的内部实现。
绕过名称改写
名称改写的设计目标是避免意外冲突,而非防止恶意访问。通过改写后的名称,仍然可以从外部访问:
class Secret:
def __init__(self):
self.__password = '123456'
s = Secret()
# print(s.__password) # AttributeError
print(s._Secret__password) # 123456 —— 可以访问,但不应这样做
这种能力在调试器和单元测试中偶尔有用,但生产代码中直接访问改写后的名称是糟糕的做法。
特殊边界情况
名称改写仅发生在经过字节码编译的类定义内部。传递给 exec() 或 eval() 的字符串代码不会将调用处的类名视为当前类:
class C:
__x = 1
code = "print(self.__x)"
# exec(code, {'self': C()}) # AttributeError: 不会触发名称改写
同样,getattr()、setattr() 和 delattr() 以及直接操作 __dict__ 时,也不会自动进行名称改写:
class D:
def __init__(self):
self.__y = 2
d = D()
# getattr(d, '__y') # AttributeError
print(getattr(d, '_D__y')) # 2 —— 必须提供改写后的名称
属性访问控制的其他手段
除了命名约定和名称改写,还可以通过 property 装饰器(见属性装饰器章节)或自定义 __getattr__、__getattribute__、__setattr__ 等魔术方法来控制属性访问。但这些属于更高级的元编程技术,日常开发中应优先使用简单的命名约定。
class Controlled:
def __init__(self):
self._internal = 0
def __setattr__(self, name, value):
if name == 'internal':
raise AttributeError("use _internal instead")
super().__setattr__(name, value)
c = Controlled()
# c.internal = 1 # AttributeError: use _internal instead
需要强调的是,Python 的哲学是"我们都是负责任的成年人"(we're all consenting adults)。语言层面不强制封装,而是通过约定和文档来引导正确的使用方式。