python 类中函数名前后加下划线的具体使用

 更新时间:2024年01月17日 10:48:47   作者:mob64ca12ed7b35  
在Python编程语言中,函数名前后有下划线是一种常见的命名约定,,被广泛应用于类中的函数,本文将介绍下划线命名风格的由来、使用场景以及如何正确应用它,感兴趣的可以了解一下

在Python编程语言中,函数名前后有下划线是一种常见的命名约定,被广泛应用于类中的函数。这种命名约定被称为“下划线命名风格”或“Snake Case”,它有助于提高代码的可读性和可维护性。本文将介绍下划线命名风格的由来、使用场景以及如何正确应用它。

下划线命名风格的由来

下划线命名风格起源于Python社区的一种共识,旨在使代码更易读、易理解、易维护。在Python社区中,强调代码的可读性,并推崇一致性和语义明确的命名风格。下划线命名风格是一种约定俗成的规范,广泛应用于Python编程中。

下划线命名风格的核心思想是通过在函数名前后添加下划线来增强函数的可读性。这种命名方式使得函数名更加清晰、易于理解,并且能够与其他命名方式进行区分。

使用下划线命名风格的场景

下划线命名风格适用于类中的函数、方法和变量。下面是一些常见的使用场景:

1. 类中的私有函数和变量

在Python中,没有严格的私有函数和变量的概念。然而,通过在函数名前面添加一个下划线,可以向其他开发者传达出这个函数或变量是类内部使用的信号。这样做有助于防止其他开发者在未经允许的情况下修改或访问这些函数和变量。

class MyClass:
    def _private_function(self):
        # 这是一个私有函数,只能在类内部访问
        pass

    def public_function(self):
        # 这是一个公共函数,可以从类外部访问
        pass

2. 类中的保护函数和变量

在Python中,通过在函数名前面添加双下划线(例如__function_name)可以创建受保护的函数和变量。这意味着这些函数和变量只能在类内部或派生类中访问,而不能从类的外部访问。

class MyClass:
    def __protected_function(self):
        # 这是一个受保护的函数,只能在类内部或派生类中访问
        pass

    def public_function(self):
        # 这是一个公共函数,可以从类外部访问
        pass

3. 避免命名冲突

在大型项目中,可能存在许多不同的模块和类。通过在函数名前面添加类名或模块名作为前缀,可以避免命名冲突,并使函数的作用范围更加明确。

class MyClass:
    def my_class_function(self):
        # 这是属于 MyClass 类的函数
        pass

class AnotherClass:
    def my_class_function(self):
        # 这是属于 AnotherClass 类的函数
        pass

下划线命名风格的注意事项

虽然下划线命名风格可以提高代码的可读性和可维护性,但在使用时也需要注意以下几点:

1. 遵循PEP 8标准
PEP 8是Python代码风格指南,提供了一套一致性和规范性的编码约定。在使用下划线命名风格时,应遵循PEP 8标准,以确保代码的一致性和易读性。

2. 不要滥用下划线
下划线命名风格应该仅用于适当的场景,如私有函数、保护函数和避免命名冲突。滥用下划线可能导致代码的冗余和混乱,降低代码的可读

补:五种下划线模式

单前导下划线:_var

Python中并没有真正意义上的“私有”,类的属性的的可见性取决于属性(变量和方法)的名字。

以单下划线开头的属性(例如_spam),应被当成API中非公有的部分(但是注意,它们仍然可以被访问),一般是具体实现细节的部分,修改它们时无需对外部通知,需要提供外部接口以供外部调用。在类中,带有前导下划线的名称只是向其他程序员指示该属性或方法旨在是私有的。

引用PEP-8

_single_leading_underscore: weak “internal use” indicator. E.g. from M import * does not import objects whose name starts with an underscore.

class BaseForm(StrAndUnicode):
    ...

    def _get_errors(self):
        "Returns an ErrorDict for the data provided for the form"
        if self._errors is None:
            self.full_clean()
        return self._errors

    errors = property(_get_errors)

该代码片段来自Django源码(django/forms/forms.py)。这段代码的设计就是errors属性是对外API的一部分,如果你想获取错误详情,应该访问errors属性,而不是(也不应该)访问_get_errors方法。

(1)约定的内部变量

当涉及到变量和方法名称时,单个下划线前缀有一个约定俗成的含义。 它是对程序员的一个提示 ,意味着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中的单个下划线前缀仅仅是一个约定,而不是一个强制性的规则。至少相对于变量和方法名而言。

其可以直接访问。不过根据python的约定,应该将其视作private,而不要在外部使用它们,良好的编程习惯是不要在外部使用它

(2)模块级私有化

单下划线常用来实现模块级私有化,当我们使用“from mymodule import *”来加载模块的时候,不会加载以单下划线开头的模块属性。也就是前导下划线的确会影响从模块中导入名称的方式。

假设你在一个名为my_module的模块中有以下代码:

# This is my_module.py:

def external_func():
    return 23

def _internal_func():
    return 42

现在,如果使用通配符从模块中导入所有名称,则Python不会导入带有前导下划线的名称除非模块定义了覆盖此行为的__all__列表,模块或包中的__all__列表显式地包含了他们):

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

顺便说一下,应该避免通配符导入,因为它们使名称空间中存在哪些名称不清楚,存在重名的变量会出现混乱。 为了清楚起见,坚持常规导入更好。

与通配符导入不同,常规导入不受前导单个下划线命名约定的影响:

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

我知道这一点可能有点令人困惑。 如果你遵循PEP 8推荐,避免通配符导入,那么你真正需要记住的只有这个:

单个下划线是一个Python命名约定,表示这个名称是供内部使用的。 它通常不由Python解释器强制执行,仅仅作为一种对程序员的提示。

单末尾下划线:var_

有时候,一个变量的最合适的名称已经被一个关键字所占用。 因此,像class或def这样的名称不能用作Python中的变量名称。 在这种情况下,你可以附加一个下划线来解决命名冲突:

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

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

总之,单个末尾下划线(后缀)是一个约定,用来避免与Python关键字产生命名冲突。 PEP 8解释了这个约定。

双前导下划线:__var

双下划线用在Python类变量中是Name Mangling。这种特定的行为差不多等价于Java中的final方法(不能被挤成)和C++中的正常方法(非虚方法)。之前很多人说Python中双下划线开头表示私有。这样理解可能也不能说错,但这不是Python设计双下划线开头的初衷和目的,Python设计此的真正目的仅仅是为了避免子类覆盖父类的方法。

《Python学习手册》的说明,以双下划线开头的变量名,会自动扩张(原始的变量名会在头部加入一个下划线,然后是所在类名称,从而包含了所在类的名称。无法被继承,也无法在外部访问。必须通过改写后的变量名或函数名来访问。更像是私有变量和私有函数。当然也不是静态变量。

(1)防止子类重写父类属性

以双下划线开头并最多以一个下划线结尾的变量或函数(例如___spam),将会被替换成_classname__spam这样的形式,其中classname是当前类的类名。知道了这点之后,我们仍然可以访问到python的私有函数

the Python docs

Any identifier of the form __spam (at least two leading underscores, at most one trailing underscore) is textually replaced with _classname__spam, where classname is the current class name with leading underscore(s) stripped. This mangling is done without regard to the syntactic position of the identifier, so it can be used to define class-private instance and class variables, methods, variables stored in globals, and even variables stored in instances. private to this class on instances of other classes.

和来自同一页面的警告:

Name mangling is intended to give classes an easy way to define “private” instance variables and methods, without having to worry about instance variables defined by derived classes, or mucking with instance variables by code outside the class. Note that the mangling rules are designed mostly to avoid accidents; it still is possible for a determined soul to access or modify a variable that is considered private.

到目前为止,我们所涉及的所有命名模式的含义,来自于已达成共识的约定。 而对于以双下划线开头的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

双下划线名称修饰对程序员是完全透明的。 下面的例子证实了这一点:

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'"

名称修饰是否也适用于方法名称? 是的,也适用。名称修饰会影响在一个类的上下文中,以两个下划线字符("dunders")开头的所有名称:

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

这是另一个也许令人惊讶的运用名称修饰的例子:

_MangledGlobal__mangled = 23

class MangledGlobal:
    def test(self):
        return __mangled

>>> MangledGlobal().test()
23

在这个例子中,我声明了一个名为_MangledGlobal__mangled的全局变量。然后我在名为MangledGlobal的类的上下文中访问变量。由于名称修饰,我能够在类的test()方法内,以__mangled来引用_MangledGlobal__mangled全局变量。 Python解释器自动将名称__mangled扩展为_MangledGlobal__mangled,因为它以两个下划线字符开头。这表明名称修饰不是专门与类属性关联的。它适用于在类上下文中使用的两个下划线字符开头的任何名称。 

(2)示例

class A(object): 
    def __method(self):
        print("I'm a method in class A")
    def method_x(self):
        print("I'm another method in class A\n")
    def method(self):
        self.__method()
        self.method_x()

class B(A):    
    def __method(self):
        print("I'm a method in class B")
    def method_x(self):
        print("I'm another method in class B\n")

if __name__ == '__main__':
    
    print("situation 1:")
    a = A()
    a.method()

    b = B()
    b.method()

    print("situation 2:")
    # a.__method()
    a._A__method() 

执行结果:

situation 1:
I'm a method in class A
I'm another method in class A
 
I'm a method in class A
I'm another method in class B
 
situation 2:
I'm a method in class A

这里有两个点需要注意:

A类中我们定义了__method()、method_x和method()三个方法;然后我们重新定义一个类B,继承自A,并且在B类中覆写(override)了其父类的__method()和method_x方法,但是从输出结果看,B对象调用method()方法时调用了其父类A的__method()方法和自己的method_x()方法。也就是说,__method()覆写没有生效,而method_x()覆写生效了。而这也正是Python设计双下划线开头的唯一目的。

前面我们就说了,Python中不存在真正意义上的私有变量。对于双下划线开头的方法和属性虽然我们不能直接引用,那是因为Python默认在其前面加了前缀_类名,所以就像situation 2下面的代码,虽然我们不能用a直接访问__method(),但却可以加上前缀去访问,即_A__method()。

Python的私有变量轧压:

Python把以两个或以上下划线字符开头且没有以两个或以上下划线结尾的变量当作私有变量。私有变量会在代码生成之前被转换为长格式(变为公有)。转换机制是这样的:在变量前端插入类名,再在前端加入一个下划线字符。这就是所谓的私有变量轧压(Private name mangling)

注意:一是因为轧压会使标识符变长,当超过255的时候,Python会切断,要注意因此引起的命名冲突。
二是当类名全部以下划线命名(如___)的时候,Python就不再执行轧压。

具体原因:因为类A定义了一个私有成员函数(变量),所以在代码生成之前先执行私有变量轧压。轧压之后,类A的代码就变成这样了:

class A(object):   
    def _A__method(self): # 这行变了
        print("I'm a method in class A")
    def method_x(self):
        print("I'm another method in class A\n")
    def method(self):
        self._A__method() # 这行变了
        self.method_x()

有点像C语言里的宏展开。因为在类B定义的时候没有覆盖__init__方法,所以调用的仍然是A.__init__,即执行了self._A__method(),自然输出“I'm a method in class A”了。

下面的两段代码可以增加说服力,增进理解:

class A(object):
       def __init__(self):
              self.__private()
              self.public()
       def __private(self):
              print 'A.__private()'
       def public(self):
              print 'A.public()'
class B(A):
       def __private(self):
              print 'B.__private()'
       def public(self):
              print 'B.public()'
b = B()


A.__private()
B.public()

原因与上述相同。类 A里的__private标识符将被转换为_A__private,这就是_A__private和__private消失的原因了。 

class C(A):
    def __init__(self):          # 重写 __init__ ,不再调用 self._A__private
        self.__private()       # 这里绑定的是 _C_private
        self.public()
    def __private(self):
        print 'C.__private()'
    def public(self):
        print 'C.public()'
c = C()


C.__private()
C.public()

可以看出重写 __init__ ,不再调用 self._A__private,而是调用类C里__init__函数里绑定的_C_private。

而且在类A中也可以直接调用self._A__method(),虽然该变量未定义,但是进行命名改编时会修改。

双前导和末尾下划线:__var__

如果一个名字同时以双下划线开始和结束,则不会应用名称修饰。 由双下划线前缀和后缀包围的变量不会被Python解释器修改:

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

>>> PrefixPostfixTest().__bam__
42

但是,Python保留了有双前导和双末尾下划线的名称,用于特殊用途。 前后有双下划线表示的是特殊函数。通常可以复写这些方法实现自己所需要的功能。

这样的例子有,__init__ --- 对象构造函数__call__ --- 它使得一个对象可以被调用。表示这是Python自己调用的,程序员不要调用。比如我们可以调用len()函数来求长度,其实它后台是调用了__len__()方法。一般我们应该使用len,而不是直接使用__len__()。

我们一般称__len__()这种方法为magic methods(魔术方法),一些操作符后台调用的也是也是这些magic methods,比如+后台调用的是__add__,-调用的是__sub__,所以这种机制使得我们可以在自己的类中覆写操作符(见后面例子)。另外,有的时候这种开头结尾双下划线方法仅仅是某些特殊场景的回调函数,比如__init__()会在对象的初始化时调用,__new__()会在构建一个实例的时候调用等等。

  • 类内置函数和全局内置函数
  • 标识符等的实现细节
  • 回调函数
class CrazyNumber(object):
    def __init__(self, n): 
        self.n = n 
    def __add__(self, other): 
        return self.n - other 
    def __sub__(self, other): 
        return self.n + other 
    def __str__(self): 
        return str(self.n) 

num = CrazyNumber(10) 
print(num) # output is: 10
print(num + 5) # output is: 5
print(num - 20) # output is: 30

在上面这个例子中,我们重写了+-操作符,将他们的功能交换了。

单下划线:_

(1)临时变量

按照习惯,有时候单个独立下划线是用作一个名字,来表示某个变量是临时的或无关紧要的。作为临时性的名称使用,但是在后面不会再次用到该名称。这种用法在循环中会经常用到。

例如,在下面的循环中,我们不需要访问正在运行的索引,我们可以使用“_”来表示它只是一个临时值:

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

你也可以在拆分(unpacking)表达式(对元组以逗号分隔开的表达式)中将单个下划线用作“不关心的”变量,以忽略特定的值。 同样,这个含义只是“依照约定”,并不会在Python解释器中触发特殊的行为。 单个下划线仅仅是一个有效的变量名称,会有这个用途而已。

在下面的代码示例中,我将汽车元组拆分为单独的变量,但我只对颜色和里程值感兴趣。 但是,为了使拆分表达式成功运行,我需要将包含在元组中的所有值分配给变量。 在这种情况下,“_”作为占位符变量可以派上用场:

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

>>> color
'red'
>>> mileage
3812.4
>>> _
12

(2)_代表交互式解释器会话中上一条的执行结果

除了用作临时变量之外,“_”是大多数Python REPL中的一个特殊变量,它表示由解释器执行的最近一个表达式的结果。这种用法有点类似于Linux中的上一条命令的用法。只不过在在Python解释器中表示的上一条执行的结果。这种用法首先被标准CPython解释器采用,然后其他类型的解释器也先后采用。

这样就很方便了,比如你可以在一个解释器会话中访问先前计算的结果,或者,你是在动态构建多个对象并与它们交互,无需事先给这些对象分配名字:

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

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

(3)国际化

也许你也曾看到”_“会被作为一个函数来使用。这种情况下,它通常用于实现国际化和本地化字符串之间翻译查找的函数名称,这似乎源自并遵循相应的C约定。

from django.utils.translation import ugettext as _
from django.http import HttpResponse

def my_view(request):
    output = _("Welcome to my site.")
    return HttpResponse(output)

场景一和场景三中的使用方法可能会相互冲突,所以我们需要避免在使用“_”作为国际化查找转换功能的代码块中同时使用“_”作为临时名称。

到此这篇关于python 类中函数名前后加下划线的具体使用的文章就介绍到这了,更多相关python 函数名加下划线内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论