⼀⽂看懂Python变量命名各种下划线的含义和区别
分享⼀篇⽂章:The Meaning of Underscores in Python。
本⽂介绍了Python中单下划线和双下划线的各种含义和命名约定,名称修饰(name mangling)的⼯作原理,以及它如何影响你⾃⼰的Python类。
单下划线和双下划线在Python变量和⽅法名称中都各有其含义。有⼀些含义仅仅是依照约定,被视作是对程序员的提⽰ - ⽽有⼀些含义是由Python解释器严格执⾏的。
如果你想知道“Python变量和⽅法名称中单下划线和双下划线的含义是什么?”,我会尽我所能在这⾥为你解答。
在本⽂中,我将讨论以下五种下划线模式和命名约定,以及它们如何影响Python程序的⾏为:
单前导下划线:_var
单末尾下划线:var_
双前导下划线:__var
双前导和末尾下划线:__var__
单下划线:_
在⽂章结尾处,你可以到⼀个简短的“速查表”,总结了五种不同的下划线命名约定及其含义,以及⼀个简短的视频教程,可让你亲⾝体验它们的⾏为。
让我们马上开始!
1. 单前导下划线 _var
当涉及到变量和⽅法名称时,单个下划线前缀有⼀个约定俗成的含义。它是对程序员的⼀个提⽰ - 意味着Python社区⼀致认为它应该是什么意思,但程序的⾏为不受影响。
下划线前缀的含义是告知其他程序员:以单个下划线开头的变量或⽅法仅供内部使⽤。该约定在PEP 8中有定义。
这不是Python强制规定的。Python不像Java那样在“私有”和“公共”变量之间有很强的区别。这就像有⼈提出了⼀个⼩⼩的下划线警告标志,说:
“嘿,这不是真的要成为类的公共接⼝的⼀部分。不去管它就好。“
看看下⾯的例⼦:
class Test:
def __init__(self):
self.foo = 11
self._bar = 23
如果你实例化此类,并尝试访问在__init__构造函数中定义的foo和_bar属性,会发⽣什么情况?让我们来看看:
>>> t = Test()
>>> t.foo
11
>>> t._bar
23
你会看到_bar中的单个下划线并没有阻⽌我们“进⼊”类并访问该变量的值。
这是因为Python中的单个下划线前缀仅仅是⼀个约定 - ⾄少相对于变量和⽅法名⽽⾔。但是,前导下划线的确会影响从模块中导⼊名称的⽅式。
假设你在⼀个名为my_module的模块中有以下代码:
# This is my_module.py:
def external_func():
return 23
def _internal_func():
return 42
现在,如果使⽤通配符从模块中导⼊所有名称,则Python不会导⼊带有前导下划线的名称(除⾮模块定义了覆盖此⾏为的__all__列表):
>>> from my_module import *
>>> external_func()
23
>>> _internal_func()
NameError: "name '_internal_func' is not defined"
顺便说⼀下,应该避免通配符导⼊,因为它们使名称空间中存在哪些名称不清楚。为了清楚起见,坚持常规导⼊更好。
与通配符导⼊不同,常规导⼊不受前导单个下划线命名约定的影响:
>>> import my_module
>>> al_func()
23
>>> my_module._internal_func()
42
我知道这⼀点可能有点令⼈困惑。如果你遵循PEP 8推荐,避免通配符导⼊,那么你真正需要记住的只有这个:
单个下划线是⼀个Python命名约定,表⽰这个名称是供内部使⽤的。它通常不由Python解释器强制执⾏,仅仅作为⼀种对程序员的提⽰。
2. 单末尾下划线 var_
有时候,⼀个变量的最合适的名称已经被⼀个关键字所占⽤。因此,像class或def这样的名称不能⽤作Python中的变量名称。在这种情况下,你可以附加⼀个下划线来解决命名冲突:
>>> def make_object(name, class):
SyntaxError: "invalid syntax"
>>> def make_object(name, class_):
... pass
java python是什么意思总之,单个末尾下划线(后缀)是⼀个约定,⽤来避免与Python关键字产⽣命名冲突。PEP 8解释了这个约定。
3. 双前导下划线 __var
到⽬前为⽌,我们所涉及的所有命名模式的含义,来⾃于已达成共识的约定。⽽对于以双下划线开头的Python类的属性(包括变量和⽅法),情况就有点不同了。
双下划线前缀会导致Python解释器重写属性名称,以避免⼦类中的命名冲突。
这也叫做名称修饰(name mangling) - 解释器更改变量的名称,以便在类被扩展的时候不容易产⽣冲突。
我知道这听起来很抽象。因此,我组合了⼀个⼩⼩的代码⽰例来予以说明:
class Test:
def __init__(self):
self.foo = 11
self._bar = 23
self.__baz = 23
让我们⽤内置的dir()函数来看看这个对象的属性:
>>> 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']
以上是这个对象属性的列表。让我们来看看这个列表,并寻我们的原始变量名称foo,_bar和__baz - 我保证你会注意到⼀些有趣的变化。
self.foo变量在属性列表中显⽰为未修改为foo。
self._bar的⾏为⽅式相同 - 它以_bar的形式显⽰在类上。就像我之前说过的,在这种情况下,前导下划线仅仅是⼀个约定。给程序员⼀个提⽰⽽已。
然⽽,对于self.__baz⽽⾔,情况看起来有点不同。当你在该列表中搜索__baz时,你会看不到有这个
名字的变量。
__baz出什么情况了?
如果你仔细观察,你会看到此对象上有⼀个名为_Test__baz的属性。这就是Python解释器所做的名称修饰。它这样做是为了防⽌变量在⼦类中被重写。
让我们创建另⼀个扩展Test类的类,并尝试重写构造函数中添加的现有属性:
class ExtendedTest(Test):
def __init__(self):
super().__init__()
self.foo = 'overridden'
self._bar = 'overridden'
self.__baz = 'overridden'
现在,你认为foo,_bar和__baz的值会出现在这个ExtendedTest类的实例上吗?我们来看⼀看:
>>> t2 = ExtendedTest()
>>> t2.foo
'overridden'
>>> t2._bar
'overridden'
>>> t2.__baz
AttributeError: "'ExtendedTest' object has no attribute '__baz'"
等⼀下,当我们尝试查看t2 .__ baz的值时,为什么我们会得到AttributeError?名称修饰被再次触发了!事实证明,这个对象甚⾄没有
__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']
正如你可以看到__baz变成_ExtendedTest__baz以防⽌意外修改:
>>> t2._ExtendedTest__baz
'overridden'
但原来的_Test__baz还在:
>>> t2._Test__baz
42
双下划线名称修饰对程序员是完全透明的。下⾯的例⼦证实了这⼀点:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论