发布于2026-07-06 阅读(0)
扫一扫,手机访问
在Python面向对象编程中,双下划线(__)前缀的属性或方法常被称作“私有成员”。但如果你以为它能像Ja va的private那样提供真正的访问控制,那可就理解错了——双下划线前缀触发的是一种编译期重命名机制,称为**名称修饰(Name Mangling)**。不少开发者一开始都以为这下子安全了,结果在实际使用中遭遇无法访问、继承失效、测试困难等麻烦。

这篇文章会带你深入剖析名称修饰的底层原理、设计初衷、常见陷阱,并给出清晰的使用指南和替代方案。
class Parent:
def __init__(self):
self.__secret = 42 # 双下划线属性
def reveal(self):
return self.__secret
p = Parent()
print(p.reveal()) # 42 (内部访问正常)
print(p.__secret) # AttributeError: 'Parent' object has no attribute '__secret'
外部直接访问 p.__secret 失败,看起来像是实现了“私有”。但真相是,你只需要知道它的“真名”就行了:
print(p._Parent__secret) # 42 ← 只是换了个名字!
class Child(Parent):
def __init__(self):
super().__init__()
self.__secret = 100 # 本意是覆盖父类的 __secret
c = Child()
print(c.reveal()) # 42 ← 为什么不是 100?
print(c._Parent__secret) # 42
print(c._Child__secret) # 100
子类定义的 __secret 并没有覆盖父类的同名属性,而是创建了一个独立的新属性。
其规则很简单:当Python编译器在类定义体内遇到以至少两个下划线开头、最多一个下划线结尾的标识符时,就会自动将其重命名为:
_ClassName__identifier
ClassName 是当前类名(定义该标识符的类)。__init__、__call__),则不进行修饰。class MyClass:
__value = 1
print(MyClass.__dict__)
# {'_MyClass__value': 1, ...}
可以看到,类的 __dict__ 中实际存储的是修饰后的名称。
名称修饰仅对在类定义体内直接出现的标识符有效。通过字符串赋值(如 setattr)或直接在 __dict__ 中插入的键,是不会有修饰效果的。
class Test:
def __init__(self):
self.__attr = 10 # 修饰为 _Test__attr
setattr(self, '__attr2', 20) # 不修饰,保持 '__attr2'
t = Test()
print(t._Test__attr) # 10
print(t.__attr2) # 20 (可正常访问)
很多人以为名称修饰是为了实现类似Ja va的private访问控制,其实不然。它的真正目的是:
在继承体系中,防止子类无意中覆盖父类的“私有”属性。
假设你开发了一个可继承的基类,内部使用了一个临时变量名 __cache。你希望子类可以自由定义自己的 __cache,而不会破坏基类的内部逻辑。名称修饰恰好提供了这种隔离。
class FrameworkBase:
def __init__(self):
self.__cache = {}
def operation(self):
self.__cache['key'] = 'value' # 始终操作基类的 __cache
class UserSubclass(FrameworkBase):
def __init__(self):
super().__init__()
self.__cache = [] # 无意或有意使用同名属性,不会影响基类
obj = UserSubclass()
obj.operation() # 基类逻辑正常工作
如果没有名称修饰,UserSubclass 的 __cache 会覆盖基类的 self.__cache,导致基类的 operation 方法操作了错误的列表,引发类型错误或逻辑混乱。
class BankAccount:
def __init__(self, balance):
self.__balance = balance
account = BankAccount(1000)
# 攻击者只需知道修饰规则即可绕过
account._BankAccount__balance = 0
结论: 名称修饰不能用来保护敏感数据或作为安全边界。任何有权访问对象的人都可以通过修饰后的名称读写属性。
class Base:
def __method(self):
return "Base"
def call(self):
return self.__method()
class Derived(Base):
def __method(self):
return "Derived"
d = Derived()
print(d.call()) # 输出 "Base",而非 "Derived"
由于名称修饰,Base.__method 变成 _Base__method,而 Derived.__method 变成 _Derived__method。基类的 call 方法始终调用 _Base__method,无法被子类覆盖。
如果需要子类重写,应使用单下划线(_method)表示“受保护”的约定。
class MyClass:
def __init__(self):
self.__value = 10
obj = MyClass()
print(getattr(obj, '__value')) # AttributeError
print(getattr(obj, '_MyClass__value')) # 10
在使用 hasattr、getattr、setattr 等动态函数时,必须手动构造修饰后的名称,增加了不必要的复杂性。
def add_property(cls):
cls.__secret = 42 # 修饰为 _add_property__secret ?不,这里是全局作用域
return cls
@add_property
class MyClass:
pass
print(MyClass.__secret) # 42 ← 竟然没被修饰!
名称修饰仅在类定义体内部触发。在类外部的函数或装饰器中赋值,双下划线前缀不会被修饰,因为它不在类的词法作用域内。
class Service:
def __init__(self):
self.__clients = []
# 测试代码
def test_service():
s = Service()
s._Service__clients.append('mock') # 丑陋且脆弱
一旦类名重构,所有测试中的修饰名都需同步修改。
| 前缀 | 行为 | 语义约定 | 适用场景 |
|---|---|---|---|
_name | 无名称修饰 | “受保护的”内部实现,不应在外部使用(但 Python 不强制) | 供子类继承或重写的内部方法/属性 |
__name | 名称修饰 | 避免子类命名冲突,强制隔离 | 基类内部真正不希望被子类意外覆盖的成员 |
__name__ | 无修饰(系统保留) | 特殊方法(魔法方法),由 Python 语言定义 | 操作符重载、协议实现 |
最佳实践:
_ 表示“请勿直接访问”。__。在类内部,直接使用原始的 __name 即可,编译器会自动修饰。
class MyClass:
def __init__(self):
self.__data = []
def add(self, item):
self.__data.append(item) # 自动修饰为 _MyClass__data
如果你确实需要从外部访问(比如调试、测试),显式使用修饰后的名称,并注释说明:
obj._MyClass__data # 访问前应三思,并做好类名变更的准备
除非你明确想要一个完全独立于父类的属性(不覆盖),否则应使用单下划线版本。
无论是单下划线还是双下划线,都应在文档字符串中说明其用途和稳定性承诺。
class Thermometer:
def __init__(self):
self._celsius = 0
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero")
self._celsius = value
def create_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
在模块中,以单下划线开头的名称不会被 from module import * 导入(除非定义了 __all__)。
print(dir(obj)) # 列出所有属性,包括修饰后的名称
现代IDE(PyCharm、VS Code)会识别双下划线成员,并在自动补全时提示修饰后的名称。
import inspect print(inspect.getsource(MyClass)) # 查看原始定义,未被修饰
pylint 提供 W0238: Attribute name __xxx__ is a special method 等警告。flake8 插件可检测到可疑的双下划线用法。| 特性 | 双下划线 __name | 单下划线 _name |
|---|---|---|
| 名称修饰 | ✅ 是 | ❌ 否 |
| 防止外部访问 | ❌ 不能真正阻止,只是改名 | ❌ 纯约定 |
| 防止子类覆盖 | ✅ 是主要目的 | ❌ 子类可自由覆盖 |
| 访问复杂度 | 需构造修饰名 | 直接访问 |
| 适用场景 | 框架基类内部隔离 | 绝大多数内部实现 |
| Python 社区推荐度 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
双下划线名称修饰是Python提供的一个精巧的工具,用于解决特定场景下的命名冲突问题。然而,将其误解为“强制私有”或“安全边界”是危险的。记住核心原则:
遵循这些原则,你既能享受名称修饰带来的继承安全性,又能避免不必要的代码复杂性和维护负担。在绝大多数日常开发中,良好的命名约定和团队规范,远比语言层面的“花招”更为可靠。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8