本文将解释Python中单下划线和双下划线(“dunder”)的各种含义和命名约定,名称修饰的工作原理,以及它对您的影响。自己的Python类。单下划线和双下划线在Python变量和方法名称中都有自己的含义。有些含义只是约定俗成,被认为是对程序员的提示——而其他含义则由Python解释器严格执行。在本文中,我讨论了以下五种下划线模式和命名约定,以及它们如何影响Python程序的行为:_在文章的最后,您可以找到一个简短的备忘单,总结了五种不同的下划线命名约定及其含义,让我们马上开始吧!1.单下划线_var在变量名和方法名中,单下划线前缀具有约定俗成的含义。它是对程序员的提示:表示Python社区同意它应该表示的意思,但程序的行为不受影响。下划线前缀的意义是告知其他程序员,单下划线开头的变量或方法仅供内部使用。此约定在PEP8中定义。这不是由Python强制执行的。Python不像Java那样在“私有”和“公共”变量之间有很强的区别。就像有人举起一个小下划线警告标志说,“嘿,这不会真正成为类公共界面的一部分。别管它了。”参见示例:classTest:def__init__(self):self.foo=11self._bar=23如果您实例化此类并尝试访问__init__构造函数中定义的foo和_bar属性,会发生什么情况?让我们看看:>>>t=Test()>>>t.foo11>>>t._bar23你会看到_bar中的单个下划线并没有阻止我们“进入”类并访问它的值多变的。这是因为Python中的单个下划线前缀只是一种约定——至少在变量和方法名称方面是这样。但是,前导下划线确实会影响名称从模块中导入的方式。假设在名为my_module的模块中有以下代码:#Thisismy_module.py:defexternal_func():return23def_internal_func():return42现在,如果您使用通配符从模块中导入所有名称,Python将不会导入名称(除非模块定义覆盖此行为的__all__列表):>>>frommy_moduleimport*>>>external_func()23>>>_internal_func()NameError:"name'_internal_func'isnotdefined"顺便说一句,应该避免导入通配符,因为它们使不清楚名称空间中存在哪些名称。为清楚起见,最好坚持使用常规导入。与通配符导入不同,常规导入不受前导单下划线命名约定的约束:>>>importmy_module>>>my_module.external_func()23>>>my_module._internal_func()42我知道这可能有点令人困惑。如果您遵循PEP8的建议来避免通配符导入,那么您真正需要记住的是:单个下划线是Python命名约定,表示该名称供内部使用。它通常不是由Python解释器强制执行的,而只是作为对程序员的提示。2.尾部单下划线var_有时最合适的变量名已经被关键字占用。因此,在Python中不能将class或def之类的名称用作变量名。在这种情况下,您可以附加一个下划线来解决命名冲突:>>>defmake_object(name,class):SyntaxError:"invalidsyntax">>>defmake_object(name,class_):...pass简而言之,单个尾随下划线(后缀)是一种约定,用于避免与Python关键字的命名冲突。PEP8解释了这个约定。3.双前导下划线__var到目前为止,我们介绍的所有命名模式的含义都来自商定的约定。对于以双下划线开头的Python类(包括变量和方法)的属性,情况有点不同。双下划线前缀会导致Python解释器重写属性名称以避免子类中的命名冲突。这也称为名称修改——解释器更改变量的名称,以便在扩展类时不太可能发生冲突。我知道这听起来很抽象。所以我整理了一个小代码示例来说明:classTest:def__init__(self):self.foo=11self._bar=23self.__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类的类,并尝试覆盖构造函数中添加的现有属性:classExtendedTest(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.__bazAttributeError:"'ExtendedTest'objecthasnoattribute'__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。;>t2._Test__baz42双下划线名称修改对程序员来说是完全透明的。下面的例子证实了这一点:“'ManglingTest'对象没有属性'__mangled'”名称改编是否也适用于方法名称?是的,它确实。名称重整会影响类上下文中所有以两个下划线字符(“双下划线”)开头的名称:classMangledMethod:def__method(self):return42defcall_it(self):returnsself.__method()>>>MangledMethod()。__method()AttributeError:"'MangledMethod'objecthasnoattribute'__method'">>>MangledMethod().call_it()42这是另一个可能令人惊讶地使用名称重整的示例:_MangledGlobal__mangled=23classMangledGlobal:deftest(self):return__mangled>>>MangledGlobal().test()23在这个例子中,我声明了一个名为_MangledGlobal__mangled的全局变量。然后,我在名为MangledGlobal的类的上下文中访问该变量。由于名称修改,我可以在类的test()方法中将_MangledGlobal__mangled全局变量称为__mangled。Python解释器自动将名称__mangled扩展为_MangledGlobal__mangled,因为它以两个下划线字符开头。这表明名称改编与类属性没有特别关联。它适用于类上下文中使用的以两个下划线字符开头的任何名称。有很多东西要吸收。老实说,这些例子和解释并不是我脑子里突然冒出来的。我做了一些研究和处理来弄清楚。我使用Python多年,但并不总是想到像这样的规则和特殊情况。有时,程序员最重要的技能是“模式识别”和知道到哪里寻找信息。如果此时您感到有点不知所措,请不要担心。花点时间尝试本文中的一些示例。让概念深入人心,这样您就可以理解名称修改的一般概念,以及我向您展示的其他一些行为。如果有一天你意外地遇到它们,你就会知道在文档中查找什么。4.双前导和双尾下划线_var_也许令人惊讶的是,如果一个名称的开头和结尾都是双下划线,则不会应用任何名称修饰。Python解释器不会修改被双下划线前缀和后缀包围的变量:classPrefixPostfixTest:def__init__(self):self.__bam__=42>>>PrefixPostfixTest().__bam__42但是,Python保留双前导和双尾下划线。名称,用于特殊用途。这方面的例子是,init__对象构造函数,或__call---它使一个对象可调用。这些dunder方法通常被称为魔法方法——但Python社区中的许多人(包括我自己)不喜欢这种方法。最好避免在您自己的程序中使用以双下划线(“双下划线”)开头和结尾的名称,以免与Python语言的未来更改发生冲突。5.单下划线_按照惯例,有时单下划线用作名称,表示变量是临时的或无关紧要的。例如,在下面的循环中,我们不需要访问运行索引,我们可以使用“_”表示它只是一个临时值:>>>for_inrange(32):...print('Hello,World.')您还可以在解包表达式中使用单个下划线作为“无关”变量来忽略特定值。同样,这个意思只是“按照惯例”,不会在Python解释器中触发特殊行为。单个下划线只是用于此目的的有效变量名。在下面的代码示例中,我将汽车元组拆分为单独的变量,但我只对颜色和里程值感兴趣。但是,为了使拆分表达式成功运行,我需要将元组中包含的所有值都分配给变量。在这种情况下,“_”可以作为占位符变量派上用场:>>>car=('red','auto',12,3812.4)>>>color,_,_,mileage=car>>>color'red'>>>mileage3812.4>>>_12除了用作临时变量外,“_”在大多数PythonREPL中是一个特殊变量,表示解释器结果评估的最近表达式的值。这非常方便,例如,您可以在解释器会话中访问先前计算的结果,或者您正在动态构造多个对象并与它们交互而无需预先为这些对象分配名称:>>>20+323>>>_23>>>print(_)23>>>list()[]>>>_.append(1)>>>_.append(2)>>>_.append(3)>>>_[1,2,3]总结下面是我在本文中介绍的五种Python下划线模式的简短摘要或“备忘单”:
