← 返回题目列表

Python 的单下划线 _x 和双下划线 __x 有什么区别?名称改写是什么?

中等 第 17 / 21 题 更新于 2026/07/31
Python名称改写私有属性下划线

简化版

**Python 没有真正的「私有」访问控制,而是用「下划线命名约定」表达访问意图,其中双下划线前缀会触发「名称改写(name mangling)」——这是唯一有语言机制支持的:① 单下划线 _x(「保护」约定)——只是「约定俗成的提示:这是内部使用的,请别在外部访问」,但语言不强制,你照样能访问 obj._x;② 双下划线 __x(「名称改写」)——Python 会在类内部把 __x 自动改写成 _类名__x,主要目的是「避免子类意外覆盖父类的属性」(不是为了「私有保护」)。关键:① _x 纯约定(提示别碰、from module import * 不会导入);② __x 会名称改写(class A: self.__x → 实际存成 self._A__x),子类的 __x 变成 _子类__x,所以父子类的 __x 互不冲突;③ 双下划线不是「私有」——你仍能通过改写后的名字 obj._A__x 访问(只是不方便);④ 首尾双下划线 __x__(如 __init__)是「魔术方法」,不触发名称改写。核心记忆:_x 是「内部使用」的约定(不强制)、__x 触发名称改写成 _类名__x(防子类覆盖、非私有)、__x__ 是魔术方法(不改写)。

详细版

三种下划线命名

命名含义机制
_x「内部/保护」约定纯约定(import * 不导入)
__x名称改写(防子类覆盖)改写成 _类名__x
__x__魔术方法/特殊属性不改写(Python 内部用)
x_避免和关键字冲突class_type_
class Base:
    def __init__(self):
        self.public = 1        # 公开
        self._protected = 2    # 约定:内部使用(提示别碰,但能访问)
        self.__private = 3     # 名称改写 → 实际存成 _Base__private

b = Base()
print(b.public)         # 1
print(b._protected)     # 2(约定"别碰",但语言不阻止)
# print(b.__private)    # ✗ AttributeError(因为实际叫 _Base__private)
print(b._Base__private) # 3(改写后的真名,仍能访问——不是真私有)
print(b.__dict__)       # {'public':1, '_protected':2, '_Base__private':3}

# 名称改写的用途:防止子类意外覆盖
class Parent:
    def __init__(self):
        self.__value = "parent"   # 改写成 _Parent__value
    def get_parent_value(self):
        return self.__value       # 类内部访问,自动用 _Parent__value

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.__value = "child"    # 改写成 _Child__value(不覆盖父类的!)
    def get_child_value(self):
        return self.__value       # 用 _Child__value

c = Child()
print(c.get_parent_value())  # "parent"(父类的 _Parent__value,没被覆盖)
print(c.get_child_value())   # "child"(子类的 _Child__value)
print(c.__dict__)            # {'_Parent__value':'parent', '_Child__value':'child'}

# 魔术方法 __x__ 不改写
class C:
    def __init__(self): ...   # __init__ 不会变成 _C__init__
    def __custom__(self): ... # 首尾双下划线也不改写

# 单下划线在 import * 时的作用
# 模块里 _helper 不会被 from module import * 导入(约定的私有)

# 关键字冲突用尾下划线
def func(class_, type_):      # class、type 是关键字/内置,加尾下划线
    ...

⚠️ 核心澄清:Python 里「双下划线 __x」经常被误解为「私有」,但它的真正目的是「名称改写来避免子类意外覆盖父类属性」,而不是「访问保护」——因为你仍然能通过改写后的名字 obj._类名__x 访问它,只是不方便而已;Python 的哲学是「我们都是成年人(we’re all consenting adults)」,靠约定而非强制。三个层次要分清:① _x(单下划线)纯粹是约定——「这是内部实现,外部请别依赖它」,语言完全不阻止你访问,唯一的语言效果是 from module import * 不会导入以 _ 开头的名字;② __x(双下划线、无尾部下划线)触发名称改写——编译器在类定义内部把所有 self.__x 改写成 self._类名__x__x in class Base_Base__x),这样父类和子类各自的 __x 变成不同的真名(_Parent__x vs _Child__x),从而「子类定义 __x 不会意外覆盖父类的 __x」——这才是名称改写的设计初衷(防止继承时的命名冲突,尤其是父类方法内部用了 self.__x、不希望被子类的同名属性搞乱);③ __x__(首尾都双下划线)不改写——这是「魔术方法/dunder」的命名空间(__init____str__),Python 内部保留、不做名称改写。实践建议:想表达「内部使用」用单下划线 _x(够了);只在「确实需要避免子类命名冲突」时才用双下划线 __x(较少见);别把 __x 当私有保护用(它不是)。

完整版教学

一、Python 没有真正的私有

先理解 Python 的访问控制哲学:

Python 没有 private/protected/public 关键字:
  不像 Java/C++ 有强制的访问修饰符
  → Python 靠"命名约定"表达访问意图

哲学:"We're all consenting adults here"
  (我们都是成年人)
  → 相信程序员会遵守约定,不强制阻止
  → 不搞"语言级的访问墙",用约定沟通意图

三种约定(下划线):
  public:正常命名(name、value)→ 公开 API
  _x:单下划线 → "内部使用,请别碰"(约定,不强制)
  __x:双下划线 → 名称改写(防子类覆盖,不是私有)
  __x__:首尾双下划线 → 魔术方法(Python 保留)

为什么不做强制私有:
  ① 灵活——调试、测试、元编程需要访问"内部"
  ② 信任——约定足够,成年人会守约
  ③ 简单——不增加语言复杂度

对比 Java:
  Java:private 强制(外部真的访问不了)
  Python:_x/__x 是约定/改写(外部仍能访问,只是提示/不方便)

关键认知:
  Python 的"私有"是"文化"不是"机制"
  → _x 提示别碰、__x 防冲突,都不是"访问墙"

所以 Python 无真正私有(靠命名约定):_x 提示内部、__x 名称改写、都不强制阻止访问

Python 没有 private/protected/public 关键字:不像 Java/C++ 有强制的访问修饰符、靠「命名约定」表达访问意图。哲学:「We’re all consenting adults here」(我们都是成年人):相信程序员会遵守约定、不强制阻止、用约定沟通意图。三种约定(下划线):public(正常命名、公开 API)、_x(单下划线、内部使用请别碰、约定不强制)、__x(双下划线、名称改写防子类覆盖、不是私有)、__x__(首尾双下划线、魔术方法 Python 保留)。为什么不做强制私有① 灵活(调试/测试/元编程需要访问内部)、② 信任(约定足够)、③ 简单(不增加语言复杂度)对比 Java:Java 的 private 强制(外部真访问不了)、Python 的 _x/__x 是约定/改写(外部仍能访问)。理解「Python 无真正私有(无 private 关键字、靠命名约定);哲学都是成年人(信任约定不强制);_x 提示内部、__x 名称改写、__x__魔术方法;Java private 强制、Python 约定/改写仍能访问」,就理解了 Python 的访问控制哲学。

二、单下划线 _x

理解 _x 的约定含义:

单下划线前缀 _x:约定"内部使用"
  含义:"这是内部实现细节,外部请别直接用"
  → 纯约定,语言不阻止访问

语言层面的唯一效果:
  from module import *  不会导入 _ 开头的名字
  # module.py: _helper(下划线开头)
  # from module import *  →  _helper 不被导入(约定的"模块私有")
  (但 import module 后 module._helper 仍能访问)

用途:
  ① 标记"内部方法/属性"(不是公开 API)
     class C:
         def _internal(self): ...   # 内部辅助方法
         self._cache = {}            # 内部状态
  ② 提示 API 使用者"别依赖这个"
  ③ 单独一个 _ 表示"我不关心这个值"
     for _ in range(3): ...         # 循环变量不用
     a, _, c = (1, 2, 3)            # 忽略中间值

不改变属性的存储:
  self._x = 1  → 存成 _x(名字不变,只是加了下划线)
  obj._x       → 正常访问(能访问,只是"约定别碰")

单下划线做变量名(特殊):
  _ 在交互式解释器里是"上一个表达式的结果"
  _ 常用于国际化 gettext(_("text"))

所以_x 是"内部使用"约定(不强制、能访问),唯一效果 import *不导入;单个_表示忽略

单下划线前缀 _x:约定「内部使用」——含义「这是内部实现细节、外部请别直接用」(纯约定、语言不阻止访问)。语言层面的唯一效果from module import * 不会导入 _ 开头的名字(约定的「模块私有」、但 import modulemodule._helper 仍能访问)。用途① 标记内部方法/属性(def _internalself._cache)、② 提示 API 使用者别依赖、③ 单独一个 _ 表示不关心这个值(for _ in range(3)a, _, c = (1,2,3)不改变属性的存储self._x = 1 存成 _x(名字不变、只是加了下划线、正常访问)。单下划线做变量名(特殊)_ 在交互式解释器是「上一个表达式的结果」、常用于国际化 gettext(_("text"))。理解「_x 是内部使用约定(不强制、能访问);唯一语言效果 from module import *不导入 _开头;用途标记内部/提示别依赖/单个_忽略值;不改变存储(名字不变)」,就掌握了单下划线。

三、双下划线 __x 与名称改写

理解名称改写机制:

双下划线前缀 __x(无尾部下划线):触发"名称改写"
  编译器在类内部把 __x 改写成 _类名__x

机制:
  class Base:
      def __init__(self):
          self.__x = 1     # 在 Base 里,__x 改写成 _Base__x
  → 实际存成 self._Base__x

  b = Base()
  b.__dict__   # {'_Base__x': 1}  ← 真名是 _Base__x
  b.__x        # AttributeError(没有叫 __x 的属性)
  b._Base__x   # 1(用改写后的真名能访问)

改写规则:
  在 class ClassName 内部,__名字(≥2 前导下划线、≤1 尾部下划线)
  → 改写成 _ClassName__名字
  只在"类定义内部"改写(类外部写 __x 不改写)

真正目的:防止子类意外覆盖父类属性(不是私有!)
  class Parent:
      def __init__(self): self.__x = "p"   # _Parent__x
      def show(self): return self.__x      # 类内部访问 _Parent__x
  class Child(Parent):
      def __init__(self):
          super().__init__()
          self.__x = "c"                   # _Child__x(不同!不覆盖)
  → Parent 的 _Parent__x 和 Child 的 _Child__x 互不干扰
  → 父类方法用 self.__x 时永远拿到 _Parent__x(子类改不了)

为什么需要(场景):
  父类的方法内部用了 self.__helper
  → 不希望子类无意中定义同名 __helper 把它搞乱
  → 名称改写让它们"隔离"(各自 _父类__helper / _子类__helper)

不是私有保护:
  b._Base__x 仍能访问 → 只是"不方便"、不是"访问不了"

所以__x 触发名称改写(改成_类名__x),目的防子类覆盖(非私有),仍能用_类名__x 访问

双下划线前缀 __x(无尾部下划线):触发「名称改写」——编译器在类内部把 __x 改写成 _类名__x机制class Base: self.__x = 1 → 实际存成 self._Base__xb.__dict__{'_Base__x': 1}b.__x AttributeError、b._Base__x 能访问)。改写规则:在 class ClassName 内部、__名字(≥2 前导下划线、≤1 尾部下划线)改写成 _ClassName__名字(只在类定义内部改写、类外部写 __x 不改写)。真正目的:防止子类意外覆盖父类属性(不是私有!)Parent__x_Parent__xChild__x_Child__x(互不干扰、父类方法用 self.__x 永远拿到 _Parent__x、子类改不了)。为什么需要:父类的方法内部用了 self.__helper、不希望子类无意中定义同名 __helper 把它搞乱(名称改写让它们隔离)。不是私有保护b._Base__x 仍能访问(只是不方便)。理解「__x 触发名称改写(类内部改成_类名__x、__dict__里真名是_Base__x);目的防子类覆盖(父子类__x 变成_Parent__x/_Child__x 互不干扰),不是私有;仍能用_类名__x 访问;只在类定义内部改写」,就掌握了名称改写。

四、首尾双下划线 x

理解魔术方法的命名:

首尾双下划线 __x__(前后都双下划线):魔术方法/特殊属性
  不触发名称改写!

例子:
  __init__、__str__、__repr__、__eq__、__len__(魔术方法)
  __name__、__doc__、__dict__、__class__(特殊属性)
  __all__、__version__(模块级约定)

为什么不改写:
  __x__ 是"Python 保留的、有特殊含义的"命名空间
  → 由 Python 解释器识别和调用(如 __init__ 实例化时调)
  → 如果改写成 _类名__init__,Python 就找不到了

改写的判定(精确):
  改写:≥2 前导下划线 + ≤1 尾部下划线
    __x    → 改写(_类名__x)
    __x_   → 改写(1 个尾下划线,仍改写)
  不改写:≥2 前导下划线 + ≥2 尾部下划线
    __x__  → 不改写(魔术方法)
    __     → 不改写(就俩下划线,无中间名)

自定义魔术方法(不推荐):
  别自己创造 __myname__(可能和未来 Python 的冲突)
  → __x__ 命名空间"归 Python 所有"

四种下划线命名总结:
  _x     内部约定(不强制、import * 不导入)
  __x    名称改写(防子类覆盖、_类名__x)
  __x__  魔术方法(Python 保留、不改写)
  x_     避免关键字冲突(class_、type_)

所以__x__(首尾双下划线)是魔术方法(Python 保留、不改写),别自造;改写只对__x(尾≤1)

首尾双下划线 __x__(前后都双下划线):魔术方法/特殊属性——不触发名称改写!例子:__init__/__str__/__eq__(魔术方法)、__name__/__doc__/__dict__(特殊属性)、__all__(模块级约定)。为什么不改写__x__ 是「Python 保留的、有特殊含义的」命名空间(由解释器识别调用、如 __init__ 实例化时调、改写成 _类名__init__ Python 就找不到了)。改写的判定(精确):改写(≥2 前导下划线 + ≤1 尾部下划线,__x_类名__x__x_ 仍改写)、不改写(≥2 前导 + ≥2 尾部,__x__ 不改写、__ 不改写)。自定义魔术方法(不推荐):别自己创造 __myname__(可能和未来 Python 冲突、__x__ 命名空间归 Python 所有)。四种下划线命名总结_x(内部约定)、__x(名称改写)、__x__(魔术方法)、x_(避免关键字冲突)。理解「x(首尾双下划线)是魔术方法/特殊属性(Python 保留、不改写、解释器识别调用);改写判定:≥2 前导+≤1 尾部才改写(__x/_x)、x__不改写;别自造__myname;四种_x/_x/x/x」,就掌握了魔术方法命名。

五、实践建议与陷阱

理解命名约定的实践和坑:

实践建议:

① 内部使用优先用单下划线 _x:
   够表达"内部实现、别碰"了(约定清晰、无副作用)
   class C:
       def _helper(self): ...   # 内部方法
       self._state = {}          # 内部状态

② 双下划线 __x 慎用(只在真需要防冲突时):
   ✓ 场景:基类的属性不希望被子类意外覆盖
   ✗ 别把它当"私有保护"(它不是)
   ✗ 别到处用 __x(名称改写会带来困惑)

③ 魔术方法 __x__ 别自造:
   只实现 Python 已有的(__init__/__str__...)

陷阱1:__x 在类外/子类里访问困难
   obj.__x 不行(要 obj._类名__x)
   子类想访问父类的 __x 也要用 _父类__x(不方便)

陷阱2:名称改写影响 getattr/setattr
   getattr(obj, '__x')      # 找不到(真名是 _类名__x)
   getattr(obj, '_类名__x') # 才对

陷阱3:以为 __x 是私有、外部真的访问不了
   → 错,obj._类名__x 仍能访问(不是访问墙)

陷阱4:动态属性名和改写
   在类内部用字符串动态设 __x 不会改写
   setattr(self, '__x', 1)  # 存成 '__x'(不改写!和 self.__x 不同)

陷阱5:混淆 _x 和 __x 的目的
   _x:提示"内部"(沟通意图)
   __x:防"子类命名冲突"(机制隔离)
   → 目的不同,别混

推荐做法:
  ① 90% 场景用 _x(内部约定,够用)
  ② 极少数防子类冲突用 __x
  ③ 别指望任何下划线提供"真私有"

所以实践:内部用_x(够了)、__x 慎用(只防冲突非私有)、__x__别自造;陷阱__x 外部要_类名__x

实践建议① 内部使用优先用单下划线 _x(够表达内部实现、别碰、约定清晰无副作用);② 双下划线 __x 慎用(只在真需要防冲突时)(场景基类的属性不希望被子类意外覆盖、别当私有保护、别到处用);③ 魔术方法 __x__ 别自造(只实现 Python 已有的)。陷阱陷阱1:__x 在类外/子类里访问困难obj.__x 不行、要 obj._类名__x);陷阱2:名称改写影响 getattr/setattrgetattr(obj, '__x') 找不到、要 '_类名__x');陷阱3:以为 __x 是私有外部真访问不了(错、obj._类名__x 仍能访问);陷阱4:动态属性名和改写setattr(self, '__x', 1) 存成 '__x' 不改写、和 self.__x 不同);陷阱5:混淆 _x__x 的目的_x 提示内部沟通意图、__x 防子类命名冲突机制隔离)。理解「实践:内部用_x(90%场景够了)、__x 慎用(只防冲突非私有)、__x__别自造;陷阱__x 外部/子类要用_类名__x、getattr 要真名、不是访问墙、setattr 字符串不改写、_x 和__x 目的不同」,就掌握了实践与陷阱。

六、总结与实践

总结下划线命名:

四种下划线命名:
  _x     单下划线:内部约定(不强制、import * 不导入)
  __x    双下划线:名称改写(改成 _类名__x、防子类覆盖)
  __x__  首尾双下划线:魔术方法(Python 保留、不改写)
  x_     尾下划线:避免关键字冲突(class_、type_)

核心澄清:
  Python 没有真正的私有(靠约定,不是强制)
  _x 是"提示别碰"、__x 是"防子类冲突"
  → 都不是访问墙(obj._类名__x 仍能访问 __x)

名称改写机制:
  在类内部,__x(≤1 尾下划线)→ _类名__x
  目的:父子类的 __x 隔离(防子类意外覆盖父类)
  非私有:改写后的真名仍可访问

哲学:"We're all consenting adults"
  信任约定、不强制阻止

实践建议:
  ① 内部实现/状态 → 用 _x(约定,够用,90%)
  ② 极少数防子类命名冲突 → 用 __x
  ③ 魔术方法 → 只用 Python 已有的(别自造 __x__)
  ④ 别指望下划线提供真私有

陷阱:
  __x 外部/子类访问要用 _类名__x
  setattr 字符串名不改写

核心总结:
  _x 内部约定(提示别碰)
  __x 名称改写 _类名__x(防子类覆盖、非私有)
  __x__ 魔术方法(不改写)
  Python 无真私有、靠约定

所以_x 内部约定、__x 名称改写(防子类覆盖非私有)、__x__魔术方法;Python 无真私有靠约定

四种下划线命名_x(单下划线、内部约定、不强制、import * 不导入)、__x(双下划线、名称改写成 _类名__x、防子类覆盖)、__x__(首尾双下划线、魔术方法、Python 保留、不改写)、x_(尾下划线、避免关键字冲突)。核心澄清:Python 没有真正的私有(靠约定不是强制)、_x 是提示别碰、__x 是防子类冲突(都不是访问墙、obj._类名__x 仍能访问)。名称改写机制:类内部 __x(≤1 尾下划线)→ _类名__x、目的父子类的 __x 隔离(防子类意外覆盖)、非私有。实践建议:内部实现/状态用 _x(90%)、极少数防子类冲突用 __x、魔术方法只用已有的、别指望真私有。理解「_x 内部约定(提示别碰)、__x 名称改写_类名__x(防子类覆盖非私有)、x__魔术方法(不改写)、x_避关键字;Python 无真私有靠约定;内部用_x、极少防冲突用__x、别自造__x」,就掌握了总结与实践。

记忆钩子:「Python 没有真正的『私有』访问控制,用『下划线命名约定』表达访问意图,哲学是『We’re all consenting adults(我们都是成年人)』靠约定不强制;★单下划线 _x(『内部/保护』约定):只是『约定俗成的提示——这是内部使用的请别在外部访问』,语言不强制、你照样能 obj._x,唯一语言效果是 from module import *不会导入 _开头的名字;★双下划线 __x(『名称改写 name mangling』):Python 在类内部自动把 self.__x 改写成 self._类名__x(class Base 里 __x→Base__x,dict__里真名是_Base__x),★真正目的是『避免子类意外覆盖父类的属性』——Parent 的__x 变_Parent__x、Child 的__x 变_Child__x 互不冲突,父类方法内部用 self.x 永远拿到_Parent__x(子类改不了),不是为了『私有保护』;★关键:x 不是私有——你仍能通过改写后的名字 obj.Base__x 访问(只是不方便);★首尾双下划线__x(如__init/str)是『魔术方法/特殊属性』,Python 保留、由解释器识别调用、不触发名称改写(改写规则:≥2 前导下划线+≤1 尾部下划线才改写);实践:90%场景想表达『内部使用』用单下划线_x(够了)、只在确实需要避免子类命名冲突时才用双下划线__x、别把__x 当私有保护、别自造__x(命名空间归 Python);另 x_尾下划线用于避免和关键字冲突(class/type)」

七、常见误区与追问

  • 误区:双下划线 __x 是 Python 的「私有」属性,外部访问不了。 不是真正的私有——__x 只是触发「名称改写」,Python 把它改写成 _类名__x 存储,外部访问 obj.__x 会失败(因为真名不叫 __x),但你仍然可以通过改写后的真名 obj._类名__x 访问它(只是不方便);Python 没有强制的访问控制、没有 Java 那种「private」墙;__x 的目的是「避免子类命名冲突」而非「访问保护」;想表达「私有/内部」用单下划线 _x(约定)就够了。
  • 误区:单下划线 _x 和双下划线 __x 目的一样,都是表示私有。 目的不同——_x(单下划线)是「约定」:提示「这是内部实现、外部请别依赖」,语言不阻止访问、纯粹是沟通意图;__x(双下划线)是「机制」:触发名称改写(_类名__x),目的是「避免子类无意中定义同名属性覆盖父类的属性」(父子类的 __x 被改写成不同的真名、互相隔离);一个是「提示别碰」(约定),一个是「防命名冲突」(隔离),别混为一谈;实践中表达「内部」用 _x 就够了。
  • 误区:initstr 这些也会被名称改写。 不会——首尾都是双下划线的 __x__(如 __init____str____dict__)是「魔术方法/特殊属性」的命名空间,不触发名称改写(Python 需要用固定的名字 __init__ 来识别和调用它们,如果改写成 _类名__init__ 就找不到了);名称改写只作用于「≥2 个前导下划线、且 ≤1 个尾部下划线」的名字(如 __x__x_),__x__(≥2 个尾部下划线)不改写;所以你实现魔术方法时它们的名字保持不变、能被 Python 正常识别。
  • 误区:在类里用 setattr(self, ‘__x’, v) 和 self.__x = v 效果一样。 不一样——self.__x = v 是「源代码里的属性名」、会在编译时触发名称改写(存成 _类名__x);而 setattr(self, '__x', v) 传的是「运行时的字符串 ‘__x’」、不会被名称改写(字符串是动态的、改写只发生在编译源代码时),会直接存成名为 '__x' 的属性;所以这两者存的是不同的属性(_类名__x vs '__x')、self.__x(读取时也改写)读到的是前者、读不到后者;名称改写只作用于「源码中直接写的 __x 标识符」,不作用于动态的字符串属性名。
  • 追问:Python 的单下划线、双下划线命名分别是什么含义? 主要四种:① _x(单下划线前缀)——「内部使用」的约定,提示「这是内部实现细节、外部请别直接依赖」,语言不强制(obj._x 照样能访问),唯一的语言效果是 from module import * 不会导入以下划线开头的名字;② __x(双下划线前缀、无尾部双下划线)——触发「名称改写」,Python 在类内部把它改写成 _类名__x,目的是避免子类意外覆盖父类的同名属性(不是私有保护,仍能通过 _类名__x 访问);③ __x__(首尾都双下划线)——「魔术方法/特殊属性」(如 __init____str____dict__),Python 保留、由解释器识别和调用、不触发名称改写;④ x_(尾部单下划线)——用于避免和 Python 关键字或内置名冲突(如 class_type_id_);另外单独一个 _ 常表示「忽略这个值」(for _ in range(3)a, _, c = ...)。
  • 追问:名称改写(name mangling)是怎么工作的,它的真正目的是什么? 工作机制:在类定义的内部,Python 编译器会把所有形如 __名字(至少两个前导下划线、至多一个尾部下划线)的标识符自动改写成 _类名__名字——比如在 class Base 里写 self.__x,编译后实际是 self._Base__x,存进实例字典的键也是 _Base__x;真正目的不是「私有保护」而是「避免子类意外覆盖父类的属性」:考虑一个父类 Parent 的方法内部用了 self.__helper,如果子类 Child 无意中也定义了 self.__helper,在没有名称改写的情况下子类的会覆盖父类的、可能破坏父类方法的行为;有了名称改写,Parent 里的 __helper 变成 _Parent__helper、Child 里的变成 _Child__helper,两者是不同的属性、互不干扰,父类方法用 self.__helper 时永远访问自己的 _Parent__helper(子类改不了它);所以名称改写是「为继承场景做的命名隔离」;它不提供访问保护(外部仍能用 obj._Parent__helper 访问)。
  • 追问:实践中该怎么用下划线命名,什么时候用 _x、什么时候用 __x? 绝大多数情况用单下划线 _x:想表达「这是内部实现/状态/辅助方法、不是公开 API、外部请别依赖」,用 _x 就足够清晰(def _helper(self)self._cache),它是最常用、最推荐的「内部」约定,无副作用;双下划线 __x 应该「慎用、少用」——只在「确实需要避免子类命名冲突」的特定场景才用(比如你写一个会被继承的基类、内部用了某个属性、且明确不希望任何子类无意中定义同名属性把它搞乱),而不是「想要私有」就用它(它不是私有、且名称改写会给调试、getattr/setattr、子类访问带来困惑);魔术方法 __x__ 只实现 Python 已有的(__init____str__ 等)、别自己创造新的(那个命名空间归 Python);关键心态是接受「Python 没有真正的私有」——所有下划线都是约定或机制隔离、不是访问墙,靠团队遵守约定而非语言强制。

八、加强记忆

Python 没有真正的「私有」访问控制,用「下划线命名约定」表达访问意图,哲学是「We’re all consenting adults(我们都是成年人)」靠约定不强制。单下划线 _x(「内部/保护」约定):只是「约定俗成的提示——这是内部使用的请别在外部访问」,语言不强制(你照样能 obj._x),唯一语言效果是 from module import * 不会导入 _ 开头的名字。双下划线 __x(「名称改写 name mangling」):Python 在类内部自动把 self.__x 改写成 self._类名__xclass Base__x_Base__x__dict__ 里真名是 _Base__x),真正目的是「避免子类意外覆盖父类的属性」——Parent 的 __x_Parent__x、Child 的 __x_Child__x 互不冲突,父类方法内部用 self.__x 永远拿到 _Parent__x(子类改不了),不是为了「私有保护」;关键:__x 不是私有——你仍能通过改写后的名字 obj._Base__x 访问(只是不方便)。首尾双下划线 __x__(如 __init__/__str__)是「魔术方法/特殊属性」,Python 保留、由解释器识别调用、不触发名称改写(改写规则:≥2 前导下划线 + ≤1 尾部下划线才改写)。实践:90% 场景想表达「内部使用」用单下划线 _x(够了)、只在确实需要避免子类命名冲突时才用双下划线 __x、别把 __x 当私有保护、别自造 __x__;另 x_ 尾下划线用于避免和关键字冲突(class_/type_)。一句话「_x 内部约定(提示别碰、不强制、import*不导入)、__x 触发名称改写成_类名__x(目的防子类覆盖父类属性、不是私有、仍能用_类名__x 访问)、__x__魔术方法(Python 保留不改写);Python 无真私有靠约定;内部用_x、极少防冲突用__x」。