Python代码中下划线的含义

󰃭 2017-06-04

内容意译文于下面文章, 水平有限,欢迎指正。 The Meaning of Underscores in Python Python学习群:278529278 (欢迎交流)

前言

刚学习python的时候,对代码中下划线的用法很迷惑,有单下划线,双下环线,有前缀的,后缀的,还有两个都有的。到底这些特殊的表达对代码会有那些影响呢? 单双下划线作用于变量名和方法名上,有些时候下划线的用法只是大家约定俗成的默契,有些用法则会收到python解释器的影响。 常见五种用法罗列如下:

  • 开头单下划线:_var
  • 结尾单下划线:var_
  • 开头双下划线:__var
  • 开头和结尾双下划线:__var__
  • 单一下划线:_

命名开头单下划线:_var

单下划线只是python社区一个约定俗成的规定,用来提醒程序员以单下划线开头的变量和方法仅供类内部使用,不是公共接口的一部分。 python中并没有严格的私有方法和公有方法,这里单下划线更像一个善意的提醒。

class Test:
    def __init__(self):
        self.foo = 11
        self._bar = 23

>>> t = Test()
>>> t.foo
11
>>> t._bar
23

单下划线不会阻止你访问内部变量,他不是强制,而是善意的提示,全靠自觉。

但是开头单下划线会对模块导入产生影响

# This is my_module.py:

def external_func():
    return 23

def _internal_func():
    return 42

>>> from my_module import *
>>> external_func()
23
>>> _internal_func()
NameError: "name '_internal_func' is not defined"

当使用* import的时候,python不会导入开头单下划线的变量和方法,除非这些内容在`all` 中被明确定义 当然也不建议使用这种导入方式

import单个模块,则不受影响

>>> import my_module
>>> my_module.external_func()
23
>>> my_module._internal_func()
42

结尾单下划线:var_

有的时候,有些变量命被关键字所占用,这个时候在结尾添加一个单下划线,可以避免命名冲突,使用这些更为恰当的名字。 这在pep8中有明确的定义

>>> def make_object(name, class):
SyntaxError: "invalid syntax"

>>> def make_object(name, class_):
...     pass

开头双下划线:__var

开头双下划线会使python解释器改变当前变量的名字(name mangling)从而避免子类中可能出现的命名冲突

class Test:
    def __init__(self):
        self.foo = 11
        self._bar = 23
        self.__baz = 23


>>> t = Test()
>>> dir(t)
['_Test__baz', '__class__', '__delattr__', '__dict__', '__dir__',
 '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
 '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__',
 '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
 '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
 '__weakref__', '_bar', 'foo']

使用dir()方法,我们可以得到一个对象的属性列表。我们可以列表中很轻松的找到 foo,_bar,但是__baz却好像消失了? 到底发生了什么? 其实__baz并未消失,只是被解释器修改成了 _Test__baz 这样这个变量就不会在子类中被覆盖。

class ExtendedTest(Test):
    def __init__(self):
        super().__init__()
        self.foo = 'overridden'
        self._bar = 'overridden'
        self.__baz = 'overridden'

>>> t2 = ExtendedTest()
>>> t2.foo
'overridden'
>>> t2._bar
'overridden'
>>> t2.__baz
AttributeError: "'ExtendedTest' object has no attribute '__baz'"

>>> dir(t2)
['_ExtendedTest__baz', '_Test__baz', '__class__', '__delattr__',
 '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
 '__getattribute__', '__gt__', '__hash__', '__init__', '__le__',
 '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
 '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
 '__subclasshook__', '__weakref__', '_bar', 'foo', 'get_vars']

>>> t2._ExtendedTest__baz
'overridden'

>>> t2._Test__baz
42

子类中通过开头双下划线定义的变量依然不能对象实例直接访问,但是我们通过dir()方法发现,子类中定义的__baz并未覆盖掉父类中__baz 他们分别是_ExtendedTest__baz, _Test__baz, 但是对内部实现来说,他们也是完全透明的,解释器并未做限制。

class ManglingTest:
    def __init__(self):
        self.__mangled = 'hello'

    def get_mangled(self):
        return self.__mangled

>>> ManglingTest().get_mangled()
'hello'
>>> ManglingTest().__mangled
AttributeError: "'ManglingTest' object has no attribute '__mangled'"

除了变量名,对方法名也是同样适用的

class MangledMethod:
    def __method(self):
        return 42

    def call_it(self):
        return self.__method()

>>> MangledMethod().__method()
AttributeError: "'MangledMethod' object has no attribute '__method'"
>>> MangledMethod().call_it()
42

由于命名重置(name mangling)的存在,解释器在对双下划线开头变量做展开的时候,也会使用到命名空间里面的全局变量。

_MangledGlobal__mangled = 23

class MangledGlobal:
    def test(self):
        return __mangled

>>> MangledGlobal().test()
23

开头和结尾双下划线:__var__

开头和结尾双下划线的用法又被称为魔术方法,命名重置(name mangling)对它不生效,不会修改名称。 由于魔术方法通常是python中预留的一些方法,比如 __init__ __call__ , 可以复写这些方法,但是不建议添加一些自定义魔术方法,这个可能会和将来python的改动 相冲突

class PrefixPostfixTest:
    def __init__(self):
        self.__bam__ = 42

>>> PrefixPostfixTest().__bam__
42

单一下划线:_

单一下划线作为变量是另一个约定,通常用来表示不会被使用的临时变量,只是upack时候用来占位

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

>>> car = ('red', 'auto', 12, 3812.4)
>>> color, _, _, mileage = car
>>> color
'red'
>>> mileage
3812.4
>>> _
12

在python REPL解释器下,_同时也是用来表示上次表达式结果的一个特殊变量,有点类似于shell中的 $?

>>> 20 + 3
23
>>> _
23
>>> print(_)
23

>>> list()
[]
>>> _.append(1)
>>> _.append(2)
>>> _.append(3)
>>> _
[1, 2, 3]

趣闻

双下划线(double underscores) 行话又被称为dunder(甘蔗) 究其原因 大概是程序员都很懒吧。 能用一个词,就不愿意发两个单词的音,另外 dunder 词结构上也是某种程度上double undersocres的简写。