下划线的作用
在Python的变量名和方法名中,单下划线和双下划线都有各自的含义,有的仅仅是作为约定,用于提示开发人员,而有的则对Python解释器有特殊含义。
你可能有些疑惑,在Python中变量名和方法名中的单下划线、双下划线到底有什么含义?下面我们就来看看下面五种下划线模式和命名约定,以及它们各自如何影响Python程序的行为:
- 前置单下划线:_var
- 后置单下划线:var_
- 前置双下划线:__var
- 前后双下划线:var
- 单下划线:_
前置单下划线:_var
当涉及变量名和方法名时,前置单下划线只有约定含义。它对于程序员而言是一种提示,Python社区约定好单下划线表达的是某种意思,其本身并不会影响程序的行为。
前置单下划线的意思是提示其他程序员,以单下划线开头的变量或方法只在内部使用。PEP 8中定义了这个约定(PEP 8是最常用的Python代码风格指南8),不过,这个约定对Python解释器并没有特殊含义,与Java不同,Python在“私有”和“公共”之间并没有很强的区别。
来看下面的Student类例子:
1 | class Student: |
如果实例化这个类并尝试访问在init构造函数中定义的name和_age属性,会发生什么情况?
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-ebc081a36d295ba51b127b2ec7cb29b1_720w.webp)
可以看到_age前面的单下划线并没有阻止我们访问变量的值,这是因为Python中的前置单下划线只是一个公认的约定,至少在涉及变量名和方法名时是这样的。但是前置下划线会影响从模块中导入名称的方式。假设在一个名为common.py的模块中有如下代码:
1 | def func1(): |
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-12cf8b17511d31b43d0335a7aaa77cfd_720w.webp)
现在,如果使用 from common import * 这种方式导入所有名称,Python不会导入带有前置单下划线的名称,除非在模块中定义了all列表覆盖了这个行为:
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-79fe81c973622fd0fe462c659fadd878_720w.webp)
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-d8410f078a72ac88cbc35220298a63df_720w.webp)
顺便说一下,我们应尽量避免使用号导入,因为这样就不清楚当前名称空间中存在哪些名称了,为了清楚起见,最好坚持使用常规导入方法,与号导入不同,常规导入不受前置单下划线命名约定的影响:
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-3a288684b483d59cca87f8f222500179_720w.webp)
后置单下划线:var_
有时,当某个变量的名称已被Python语言中的关键字占用,如class、def等的名称不能用作Python中的变量名,在这种情况下可以追加一个下划线来绕过命名冲突:
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-b1d0a06956ff6dded656ead57fd2a2b5_720w.webp)
用一个后置单下划线来避免与Python关键字的命名冲突是一个约定。PEP 8定义并解释了这个约定。
前置双下划线:__var
前面的两种下划线模式只有约定的意义,但使用以双下划线开头的Python类属性(变量和方法)就不一样了,双下划线前缀会让Python解释器重写属性名称,以避免子类中的命名冲突,这也称为名称改写,即解释器会更改变量的名称,以便在稍后扩展这个类时避免命名冲突,下面用代码示例来实验一下:
1 | class Student: |
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-f59f997464278c9329f3b8073d7980c0_720w.webp)
上图中我们用内置的dir()函数看这个对象的属性:该函数返回了一个包含对象属性的列表,在这个列表中尝试寻找之前的变量名称name、_age和gender,你会发现一些有趣的变化,首先,self.name变量没有改动,在属性列表中显示为name,接着,self._age也一样,在类中显示为_age,在这种情况下前置下划线仅仅是一个约定,是对程序员的一个提示,然而self.gender就不一样了,在该列表中找不到gender这个变量,仔细观察就会看到,这个对象上有一个名为_Studentgender的属性,这就是Python解释器应用名称改写之后的名称,是为了防止子类覆盖这些变量。
名称改写也适用于方法名,会影响在类环境中所有以双下划线开头的名称。
前后双下划线:var
如果名字前后都使用双下划线,则不会发生名称改写,前后由双下划线包围的变量不受Python解释器的影响:
1 | class Student: |
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-e2d7e1d431a008cebaf64f6d9c1cd850_720w.webp)
但是前后双下划线的名称在Python中有特殊用途,像init这样的对象构造函数,用来让对象可调用的call函数,都遵循这条规则。
这些双下划线方法通常被称为魔法方法,双下划线方法是Python的核心功能,应根据需要使用,但就命名约定而言,最好避免在自己的程序中使用以双下划线开头和结尾的名称,以避免与Python语言的未来变更发生冲突。
单下划线:_
按照约定,单下划线有时用作名称,来表示变量是临时的或弃用的,例如下面的循环中并不需要访问运行的索引,那么可以使用_来表示它只是一个临时值:
1 | if __name__ == "__main__": |
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-554204c0d199c6bd1363bb8a1d5cb66c_720w.webp)
在解包表达式中还可使用单下划线表示一个弃用的变量来忽略特定的值,同样,这个含义只是一个约定,不会触发Python解析器中的任何特殊行为,单下划线只是一个有效的变量名,偶尔用于该目的。
下面的代码示例中,我将元组解包为单独的变量,但其中只关注name和score字段的值。可是为了执行解包表达式,我们就必须为元组中的所有值都分配变量,此时 _ 用作占位符变量:
1 | if __name__ == "__main__": |
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-9db2ac48068b891bcd8f9b7e247f975f_720w.webp)
除了用作临时变量之外,_在大多数Python REPL中是一个特殊变量,表示由解释器计算的上一个表达式的结果,如果正在使用解释器会话,用下划线可以方便地获取先前计算的结果:
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-fc524b016205b7b1865a1f5aeeb34fc4_720w.webp)
如果正在实时构建对象,有单下划线的话不用事先指定名称就能与之交互:
![img](/2023/08/17/%E8%AE%A1%E7%AE%97%E6%9C%BA/python/v2-afbe91628a20df60688d0ef0f5e5f05b_720w.webp)
总结:
- 前置单下划线_var:命名约定,用来表示该名称仅在内部使用。一般对Python解释器没有特殊含义(*号导入除外),只能作为对程序员的提示。
- 后置单下划线var_:命名约定,用于避免与Python关键字发生命名冲突。
- 前置双下划线__var:在类环境中使用时会触发名称改写,对Python解释器有特殊含义。
- 前后双下划线var:表示由Python语言定义的特殊方法。在自定义的属性中要避免使用这种命名方式。
- 单下划线_:有时用作临时或弃用变量的名称,此外还能表示Python REPL会话中上一个表达式的结果。