单下划线和双下划线在Python变量名和方法名中都有各自的含义。有些仅仅是作为约定,用于提示开发人员;而另一些则对Python解释器有特殊含义。

  • 前置单下划线:_var
  • 后置单下划线:var_
  • 前置双下划线:__var
  • 前后双下划线:__var__
  • 单下划线:_

    1. 前置单下划线:_var

    当涉及变量名和方法名时,前置单下划线只有约定含义。它对于程序员而言是一种提示——Python社区约定好单下划线表达的是某种意思,其本身并不会影响程序的行为。
    前置下划线的意思是提示 其他程序员,以单下划线开头的变量或方法只在内部使用。PEP 8中定义了这个约定(PEP 8是最常用的Python代码风格指南8 )。
    8 详见PEP 8:“Style Guide for Python Code”。
    不过,这个约定对Python解释器并没有特殊含义。与Java不同,Python在“私有”和“公共”变量之间并没有很强的区别。在变量名之前添加一个下划线更像是有人挂出了一个小小的下划线警告标志:“注意,这并不是这个类的公共接口。最好不要使用它 。”
    来看下面的例子:

    1. class Test(object):
    2. def __init__(self):
    3. self.foo = 11
    4. self._bar = 22

    如果实例化这个类并尝试访问在__init__ 构造函数中定义的foo_bar 属性,会发生什么情况?
    我们来看看:

    1. >>> t = Test()
    2. >>> t.foo
    3. 11
    4. >>> t._bar
    5. 23

    可以看到,_bar 前面的单下划线并没有阻止我们“进入”这个类访问变量的值。
    这是因为Python中的前置单下划线只是一个公认的约定,至少在涉及变量名和方法名时是这样的。但是前置下划线会影响从模块中导入名称的方式。假设在一个名为my_module 的模块中有以下代码: ```python

    my_module.py:

def external_func(): return 23 def _internal_func(): return 42

  1. 现在,如果使用**通配符导入** 从这个模块中导入所有名称,Python**不会** 导入带有前置单下划线的名称(除非模块中定义了`__all__` 列表覆盖了这个行为**9** ):<br />**9** 详见Python文档:“Importing * From a Package”。
  2. ```python
  3. >>> from my_module import *
  4. >>> external_func()
  5. 23
  6. >>> _internal_func()
  7. NameError: "name '_internal_func' is not defined"

顺便说一下,应避免使用通配符导入,因为这样就不清楚当前名称空间中存在哪些名称了。10 为了清楚起见,最好坚持使用常规导入方法。与通配符导入不同,常规导入不受前置单下划线命名约定的影响:
10 详见PEP 8:“Imports”。

  1. >>> import my_module
  2. >>> my_module.external_func()
  3. 23
  4. >>> my_module._internal_func()
  5. 42

这里可能有点混乱。但如果你遵循PEP 8的建议不使用通配符导入,那么真正需要记住的只有下面这一条。
以单下划线开头的名称只是Python命名中的约定,表示供内部使用。它通常对Python解释器没有特殊含义,仅仅作为对程序员的提示。

2. 后置单下划线:var_

有时,某个变量最合适的名称已被Python语言中的关键字占用。因此,诸如classdef 的名称不能用作Python中的变量名。在这种情况下,可以追加一个下划线来绕过命名冲突:

  1. >>> def make_object(name, class):
  2. SyntaxError: "invalid syntax"
  3. >>> def make_object(name, class_):
  4. ... pass

总之,用一个后置单下划线来避免与Python关键字的命名冲突是一个约定。PEP 8定义并解释了这个约定。

3. 前置双下划线: __var

迄今为止,我们介绍的命名模式只有约定的意义,但使用以双下划线开头的Python类属性(变量和方法)就不一样了。
双下划线前缀会让Python解释器重写属性名称,以避免子类中的命名冲突。
这也称为名称改写 (name mangling),即解释器会更改变量的名称,以便在稍后扩展这个类时避免命名冲突。
听起来很抽象,下面用代码示例来实验一下:

  1. class Test:
  2. def __init__(self):
  3. self.foo = 11
  4. self._bar = 23
  5. self.__baz = 42

接着用内置的dir() 函数来看看这个对象的属性:

  1. >>> t = Test()
  2. >>> dir(t)
  3. ['_Test__baz', '__class__', '__delattr__', '__dict__',
  4. '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
  5. '__getattribute__', '__gt__', '__hash__', '__init__',
  6. '__le__', '__lt__', '__module__', '__ne__', '__new__',
  7. '__reduce__', '__reduce_ex__', '__repr__',
  8. '__setattr__', '__sizeof__', '__str__',
  9. '__subclasshook__', '__weakref__', '_bar', 'foo']

该函数返回了一个包含对象属性的列表。在这个列表中尝试寻找之前的变量名称foo_bar__baz ,你会发现一些有趣的变化。
首先,self.foo 变量没有改动,在属性列表中显示为foo
接着,self._bar 也一样,在类中显示为_bar 。前面说了,在这种情况下前置下划线仅仅是一个约定 ,是对程序员的一个提示。
然而self.__baz 就不一样了。在该列表中找不到__baz 这个变量。
__baz 到底发生了什么?
仔细观察就会看到,这个对象上有一个名为_Test__baz 的属性。这是Python解释器应用名称改写 之后的名称,是为了防止子类覆盖这些变量。
接着创建另一个类来扩展Test 类,并尝试覆盖之前构造函数中添加的属性:

  1. class ExtendedTest(Test):
  2. def __init__(self):
  3. super().__init__()
  4. self.foo = 'overridden'
  5. self._bar = 'overridden'
  6. self.__baz = 'overridden'

现在你认为这个ExtendedTest 类实例上的foo_bar__baz 值会是什么?来一起看看:

  1. >>> t2 = ExtendedTest()
  2. >>> t2.foo
  3. 'overridden'
  4. >>> t2._bar
  5. 'overridden'
  6. >>> t2.__baz
  7. AttributeError:
  8. "'ExtendedTest' object has no attribute '__baz'"

等一下,当试图访问t2.__baz 的值时,为什么会得到AttributeError ?因为Python又进行了名称改写!实际上,这个对象甚至没有__baz 属性:

  1. >>> dir(t2)
  2. ['_ExtendedTest__baz', '_Test__baz', '__class__',
  3. '__delattr__', '__dict__', '__dir__', '__doc__',
  4. '__eq__', '__format__', '__ge__', '__getattribute__',
  5. '__gt__', '__hash__', '__init__', '__le__', '__lt__',
  6. '__module__', '__ne__', '__new__', '__reduce__',
  7. '__reduce_ex__', '__repr__', '__setattr__',
  8. '__sizeof__', '__str__', '__subclasshook__',
  9. '__weakref__', '_bar', 'foo', 'get_vars']

可以看到,为了防止意外改动,__baz 变成了_ExtendedTest__baz ,但原来的_Test__baz 还在:

  1. >>> t2._ExtendedTest__baz
  2. 'overridden'
  3. >>> t2._Test__baz
  4. 42

程序员无法感知双下划线名称改写,下面的例子可以证实这一点:

  1. class ManglingTest:
  2. def __init__(self):
  3. self.__mangled = 'hello'
  4. def get_mangled(self):
  5. return self.__mangled
  6. >>> ManglingTest().get_mangled()
  7. 'hello'
  8. >>> ManglingTest().__mangled
  9. AttributeError:
  10. "'ManglingTest' object has no attribute '__mangled'"

名称改写也适用于方法名,会影响在类环境中所有 以双下划线(dunder)开头的名称:

  1. class MangledMethod:
  2. def __method(self):
  3. return 42
  4. def call_it(self):
  5. return self.__method()
  6. >>> MangledMethod().__method()
  7. AttributeError:
  8. "'MangledMethod' object has no attribute '__method'"
  9. >>> MangledMethod().call_it()
  10. 42

下面这个名称改写的示例可能会令人惊讶:

  1. _MangledGlobal__mangled = 23
  2. class MangledGlobal:
  3. def test(self):
  4. return __mangled
  5. >>> MangledGlobal().test()
  6. 23

这个例子先声明_MangledGlobal__mangled 为全局变量,然后在名为MangledGlobal 的类环境中访问变量。由于名称改写,类中的test() 方法仅用__mangled 就能引用_MangledGlobal__mangled 全局变量。
__mangled 以双下划线开头,因此Python解释器自动将名称扩展为_MangledGlobal__mangled 。这表明名称改写不专门与类属性绑定,而是能够应用于类环境中所有以双下划线开头的名称。
这里需要掌握的内容确实有点多。
说实话,我也没有把这些例子和解释记在大脑中,所以当初撰写这些例子的时候花了一些时间研究和编辑。虽然我有多年的Python使用经验,但大脑中并没有一直记着这样的规则和特殊情形。
有时,程序员最重要的技能是“模式识别”,以及知道查找哪些内容。如果你目前还有点不知所措,不要担心,慢慢来,继续尝试本章中的例子。
深入掌握这些概念之后,你就能识别出名称改写和刚刚介绍的其他行为给程序带来的影响。如果有一天在实际工作中遇到相关问题,你应该知道在文档中搜索哪些信息。
补充内容:什么是dunder
如果你听过一些有经验的Python高手谈论Python或者看过几次Python会议演讲,可能听说过dunder这个词。如果你还不知道这是什么意思,答案马上揭晓。
在Python社区中通常称双下划线为dunder。因为Python代码中经常出现双下划线,所以为了简化发音,Python高手通常会将“双下划线”(double underscore)简称为dunder11 。
11 后续内容中会将dunder翻译成“双下划线方法”。——译者注
例如,__baz 在英文中读作dunderbaz。与之类似,__init__ 读作dunderinit,虽然按道理说应该是dunderinitdunder。
但这只是命名约定中的另一个癖好,就像是Python开发人员的暗号

4. 前后双下划线:__var__

这也许有点令人惊讶——如果名字前后 都使用双下划线,则不会发生名称改写。前后由双下划线包围的变量不受Python解释器的影响:

  1. class PrefixPostfixTest:
  2. def __init__(self):
  3. self.__bam__ = 42
  4. >>> PrefixPostfixTest().__bam__
  5. 42

然而,同时具有前后双下划线的名称在Python中有特殊用途。像__init__ 这样的对象构造函数,用来让对象可调用的__call__ 函数,都遵循这条规则。
这些双下划线方法 通常被称为魔法方法 ,但Python社区中的许多人(包括我自己)不喜欢这个词。因为这个词像是暗示人们要退避三舍,但实际上完全不必如此。双下划线方法是Python的核心功能,应根据需要使用,其中并没有什么神奇或晦涩的内容。
但就命名约定而言,最好避免在自己的程序中使用以双下划线开头和结尾的名称,以避免与Python语言的未来变更发生冲突。

5. 单下划线:_

按照约定,单下划线有时用作名称,来表示变量是临时的或无关紧要的。
例如下面的循环中并不需要访问运行的索引,那么可以使用_ 来表示它只是一个临时值:

  1. >>> for _ in range(32):
  2. ... print('Hello, World.')

在解包表达式中还可使用单下划线表示一个“不关心”的变量来忽略特定的值。同样,这个含义只是一个约定,不会触发Python解析器中的任何特殊行为。单下划线只是一个有效的变量名,偶尔用于该目的。
下面的代码示例中,我将元组解包为单独的变量,但其中只关注colormileage 字段的值。可是为了执行解包表达式就必须为元组中的所有值都分配变量,此时_ 用作占位符变量:

  1. >>> car = ('red', 'auto', 12, 3812.4)
  2. >>> color, _, _, mileage = car
  3. >>> color
  4. 'red'
  5. >>> mileage
  6. 3812.4
  7. >>> _
  8. 12

除了用作临时变量之外,_ 在大多数Python REPL中是一个特殊变量,表示由解释器计算的上一个表达式的结果。
如果正在使用解释器会话,用下划线可以方便地获取先前计算的结果:

  1. >>> 20 + 3
  2. 23
  3. >>> _
  4. 23
  5. >>> print(_)
  6. 23

如果正在实时构建对象,有单下划线的话不用事先指定名称就能与之交互:

  1. >>> list()
  2. []
  3. >>> _.append(1)
  4. >>> _.append(2)
  5. >>> _.append(3)
  6. >>> _
  7. [1, 2, 3]

6. 关键要点

  • 前置单下划线 _var :命名约定,用来表示该名称仅在内部使用。一般对Python解释器没有特殊含义(通配符导入除外),只能作为对程序员的提示。
  • 后置单下划线 var_ :命名约定,用于避免与Python关键字发生命名冲突。
  • 前置双下划线 __var :在类环境中使用时会触发名称改写,对Python解释器有特殊含义。
  • 前后双下划线 __var__ :表示由Python语言定义的特殊方法。在自定义的属性中要避免使用这种命名方式。
  • 单下划线 _ :有时用作临时或无意义变量的名称(“不关心”)。此外还能表示Python REPL会话中上一个表达式的结果。