我应该在Python中使用名称重整吗?

150
在其他语言中,一个有用的准则是尽可能将所有内容隐藏起来以帮助生成更好的代码。如果对于一个变量是否应该是私有或受保护存在疑问,最好选择私有。
那么Python也适用相同的规则吗?我应该一开始就在所有东西上使用两个前导下划线,并且只有在需要时才使它们更少隐藏(只有一个下划线)吗?
如果惯例是只使用一个下划线,我也想知道其原理。
以下是我留在JBernardo's answer上的评论。它解释了我为什么提出这个问题,也解释了为什么我想知道Python与其他语言不同的原因:
“我来自那些训练你认为所有内容都应该尽可能公开但不超过所需的语言。原因是这将减少依赖关系并使代码更容易修改。Python做事情的方式正好相反 - 从公开开始并向隐藏方向发展 - 对我来说很奇怪。”
11个回答

239

当你有疑问时,保持“public” - 我的意思是,不要添加任何东西来隐藏你的属性名。如果你有一个带有一些内部值的类,请不要担心它。不要写成:

class Stack(object):

    def __init__(self):
        self.__storage = [] # Too uptight

    def push(self, value):
        self.__storage.append(value)

写成默认值:

class Stack(object):

    def __init__(self):
        self.storage = [] # No mangling

    def push(self, value):
        self.storage.append(value)

这绝对是一种有争议的做法。Python新手讨厌它,甚至一些老Python程序员也鄙视这个默认值——但它仍然是默认值,所以我建议你遵循它,即使你感到不舒服。
如果您真的想向用户发送“不能触摸!”的消息,通常的方法是在变量前加一个下划线。这只是一种约定,但人们理解它,并在处理这种东西时特别小心。
class Stack(object):

    def __init__(self):
        self._storage = [] # This is ok, but Pythonistas use it to be relaxed about it

    def push(self, value):
        self._storage.append(value)

这也可以很有用,避免属性名和属性名称之间的冲突:
 class Person(object):
     def __init__(self, name, age):
         self.name = name
         self._age = age if age >= 0 else 0
     
     @property
     def age(self):
         return self._age
     
     @age.setter
     def age(self, age):
         if age >= 0:
             self._age = age
         else:
             self._age  = 0

关于双下划线,我们主要使用双下划线魔术方法避免意外重载方法和与超类属性冲突。如果你编写一个需要多次扩展的类,它会非常有价值。
如果你想将其用于其他目的,也可以,但这既不寻常也不推荐。
编辑:为什么呢?因为通常Python风格并不强调使事物私有 - 相反!有许多原因 - 大部分都是有争议的... 让我们看看其中一些。
Python有属性
今天,大多数面向对象语言采用相反的方法:不应该使用的东西不应该可见,所以属性应该是私有的。理论上,这将产生更易管理、耦合度更低的类,因为没有人会鲁莽地改变对象的值。
然而,情况并不那么简单。例如,Java类有许多只获取值的getter和只设置值的setter。你需要,假设,七行代码来声明一个属性 - 这是Python程序员会说是不必要的复杂性。此外,你需要写很多代码来获取一个公共字段,因为你可以在实践中使用getter和setter改变它的值。
那么为什么要遵循这个默认私有策略呢?默认将属性设置为公共的。当然,在Java中这是有问题的,因为如果你决定向属性添加一些验证,它将需要你改变所有:
person.age = age;

在你的代码中,比方说,

person.setAge(age);

setAge() 的含义:

public void setAge(int age) {
    if (age >= 0) {
        this.age = age;
    } else {
        this.age = 0;
    }
}

所以在Java(和其他语言中),默认情况下仍然使用getter和setter,因为它们可能很烦人,但如果您发现自己处于我描述的情况中,则可以节省大量时间。

然而,在Python中您不需要这样做,因为Python有属性。如果您有这个类:

 class Person(object):
     def __init__(self, name, age):
         self.name = name
         self.age = age

如果你决定验证年龄,你不需要更改代码中的 person.age = age 部分。只需添加一个属性(如下所示)

 class Person(object):
     def __init__(self, name, age):
         self.name = name
         self._age = age if age >= 0 else 0
     
     @property
     def age(self):
         return self._age
     
     @age.setter
     def age(self, age):
         if age >= 0:
             self._age = age
         else:
             self._age  = 0

假设你可以这样做并仍然使用person.age = age,那么为什么要添加私有字段和getter和setter?
(另外,请参见Python不是Java关于使用getter和setter的危害的文章。)
一切都是可见的 - 而试图隐藏会使您的工作变得复杂。
即使在具有私有属性的语言中,您也可以通过某些反射/内省库访问它们。人们经常这样做,在框架中和解决紧急需求时。问题在于内省库只是以复杂的方式执行您可以使用公共属性完成的操作。
由于Python是一种非常动态的语言,向类添加此负担是低效的。
问题不在于无法看到 - 而在于必须看到
对于Pythonista而言,封装不是无法查看类的内部,而是可以避免查看它的可能性。封装是组件的属性,用户可以使用该属性而不必关注其内部细节。如果您可以使用组件而不必考虑其实现,则它被封装了(在Python程序员的意见中)。
现在,如果您编写了一个类,可以在不考虑实现细节的情况下使用它,则如果您出于某种原因想要查看类的内部,没有问题。重点是:您的API应该很好,其余都是细节。

Guido说了算

这并不具有争议性: 他实际上是这么说的。(查找"公开kimono")

这是文化

是的,有一些原因,但没有关键原因。这主要是Python编程中的文化方面。坦率地说,也可以反过来问:为什么有些语言默认使用私有属性?对于Python实践的同样主要原因:因为这些语言的文化不同,每种选择都有优缺点。

既然已经有了这种文化,建议您遵循它。否则,当您在Stack Overflow上提问时,您会被Python程序员告知从您的代码中删除__,这可能会让您感到恼火 :)


4
  1. 封装是为了保护类的不变量,而不是隐藏对外部世界无关的细节,因为这样做只会带来麻烦。
  2. “重点是:你的API应该是好的,其余都是细节。” 这是正确的。公共属性也是你的API的一部分。有时候公共setter是合适的(涉及到类的不变量),有时候则不是。一个具有公共setter但不应该公开的API(存在违反不变量的风险)是一个糟糕的API。这意味着你必须考虑每个setter的可见性,而拥有一个“默认”的设置是没有意义的。
- Jupiter
这甚至不是默认设置:Python根本没有不同的可见性模式。(即使名称混淆也很容易被打败,如果你真的想要的话;它的目的是避免意外覆盖继承属性。) - chepner

36

首先 - 什么是名称修饰?

当你在一个类的定义中使用__any_name__any_name_,即两个(或更多)前导下划线和最多一个尾随下划线时,就会调用名称修饰。

class Demo:
    __any_name = "__any_name"
    __any_other_name_ = "__any_other_name_"

现在是:

>>> [n for n in dir(Demo) if 'any' in n]
['_Demo__any_name', '_Demo__any_other_name_']
>>> Demo._Demo__any_name
'__any_name'
>>> Demo._Demo__any_other_name_
'__any_other_name_'

当你不确定时应该做什么?

看起来这个用法是为了防止子类使用一个类已经使用的属性。

它的潜在价值在于避免与想要覆盖行为的子类发生名称冲突,以便父类功能继续按预期工作。然而,Python文档中的示例不支持Liskov替换原则,我也没有想到任何有用的例子。

缺点是增加了阅读和理解代码库的认知负荷,尤其是在调试时,你会在源代码中看到双下划线名称,在调试器中看到被编码的名称。

我的个人方法是有意避免使用它。我在一个非常大的代码库上工作,很少使用它,而且并不合理。

你需要意识到它的存在,以便在看到时能够识别它。

PEP 8

PEP 8,Python标准库风格指南,目前说:

关于使用__names存在一些争议。

如果你的类旨在被子类化,而且你有属性不想让子类使用,请考虑将它们命名为双前导下划线和没有尾随下划线的名称。

  1. 请注意,在编码的名称中仅使用简单类名,因此,如果子类选择相同的类名和属性名,则仍然可能发生名称冲突。

  2. 名称编码可能会使某些用途(例如调试和__getattr__())变得不太方便。但是,名称编码算法已经很好地记录并且易于手动执行。

  3. 并非每个人都喜欢名称编码。请尝试找到避免意外名称冲突和高级调用者潜在用途之间的平衡。

如何使用?

如果在类定义中前置两个下划线(不要以双下划线结尾),则名称将被编码,并在对象上添加一个下划线和类名:

>>> class Foo(object):
...     __foobar = None
...     _foobaz = None
...     __fooquux__ = None
... 
>>> [name for name in dir(Foo) if 'foo' in name]
['_Foo__foobar', '__fooquux__', '_foobaz']

请注意,只有在解析类定义时才会混淆名称:

>>> Foo.__test = None
>>> Foo.__test
>>> Foo._Foo__test
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Foo' has no attribute '_Foo__test'

对于那些刚开始学习Python的人来说,当他们无法手动访问类定义中已定义的名称时,有时会很难理解发生了什么。这不是反对它的一个强有力的理由,但如果你有一个学习受众群体,这也值得考虑。

一个下划线?

如果惯例是只使用一个下划线,我也想知道其理由。

当我的意图是让用户不要碰某个属性时,我倾向于只使用一个下划线,但这是因为在我的心理模型中,子类可以访问该名称(无论如何,因为他们可以轻松地找到这个混淆后的名称)。

如果我正在审查使用__前缀的代码,我会询问他们为什么要调用名称混淆,并且如果他们可以通过使用一个下划线做得一样好,那我要记住,如果子类选择相同的类和类属性名称,尽管如此还是会发生名称冲突。


17

我不会认为实践能产生更好的代码。可见性修饰符只会使你分心,而且作为一个副作用,强制你的接口按照你的意图来使用。一般来说,执行可见性限制可以防止程序员如果他们没有正确阅读文档就搞砸事情。

一个更好的解决方案是Python所推崇的方法:您的类和变量应该有良好的文档,并清楚地说明它们的行为方式。源代码应该是可用的。这是编写代码的一种更可扩展和可靠的方式。

我在Python中的策略如下:

  1. 只是写出来,不要假设您的数据应该受到保护。这假设您编写了创建问题理想接口的代码。
  2. 对于可能不会被外部使用并且不是常规“客户端代码”接口的内容,使用前导下划线。
  3. 仅对纯粹在类内部方便的内容或者将会在意外暴露时造成重大损害的内容使用双下划线。

最重要的是,应清晰地了解每个东西的作用。如果有其他人将要使用它,请进行记录。如果您希望将来一年内它有用,请记录下来。

顺便说一句,在那些其他语言中您实际上应该使用 protected:您永远不知道您的类以后可能会被继承并且可能用于什么。最好只保护那些您确定不能或者不应由外部代码使用的变量。


10

不应该将私有数据公开,而是应该从对象接口开始考虑。也就是说,应该首先确定世界所看到的(公共的)东西,然后确定为此必要的私有内容。

其他语言很难让曾经公开的内容变成私有的。例如如果我将变量设置为私有或受保护,会破坏大量代码。但在Python中使用属性(properties)不会出现类似问题。相反,即使重新排列内部数据,我也可以保持相同的接口。

下划线和双下划线的区别是Python确实尝试强制执行后者。当然,它并没有非常努力,但确实让其变得困难。仅仅使用下划线只是告诉其他程序员意图,他们可以自由忽略这个规则,但忽略这个规则有时也是有帮助的。例如调试、临时修复和使用不打算以你所用的方式使用的第三方代码。


7
已经有很多好的答案了,但我想再提供一个。这也部分回应了那些不断说双下划线不私有(它确实是私有的)的人。
如果你看一下Java/C#,它们都有private/protected/public。所有这些都是编译时构造。它们只在编译时强制执行。如果你在Java/C#中使用反射,你可以轻松访问private方法。
现在每次你调用Python中的一个函数,你本质上就是在使用反射。在Python中,这些代码片段是相同的。
lst = []
lst.append(1)
getattr(lst, 'append')(1)

“点”语法只是后面这段代码的语法糖。主要是因为只使用一个函数调用的getattr已经很丑陋了。从那里开始,情况只会变得更糟。
所以,基于这一点,“私有”的Java/C#版本是不存在的,因为Python不编译代码。在运行时,Java和C#无法检查函数是私有还是公共的,因为该信息已经消失(它也不知道函数被从哪里调用)。
现在有了这些信息,双下划线的名称混淆是实现“私有性”最合理的方式。现在,当从“self”实例调用函数并注意到它以“__”开头时,它只是在那里执行名称混淆。这只是更多的语法糖。这种语法糖允许在仅使用反射进行数据成员访问的语言中实现“私有”。
免责声明:我从未听过任何Python开发人员说过类似的话。缺乏“私有”属性的真正原因是文化的,但您还会注意到大多数脚本/解释语言都没有私有属性。除了编译时外,强制执行私有并不实用。

5
以下代码片段将解释所有不同情况:
  • two leading underscores (__a)
  • single leading underscore (_a)
  • no underscore (a)

    class Test:
    
    def __init__(self):
        self.__a = 'test1'
        self._a = 'test2'
        self.a = 'test3'
    
    def change_value(self,value):
        self.__a = value
        return self.__a
    
打印测试对象的所有有效属性。
testObj1 = Test()
valid_attributes = dir(testObj1)
print valid_attributes

['_Test__a', '__doc__', '__init__', '__module__', '_a', 'a', 
'change_value']

在这里,你可以看到__a的名称已经被更改为_Test__a,以防止任何子类覆盖此变量。这个概念在Python中被称为“名称修饰”。

你可以像这样访问它:

testObj2 = Test()
print testObj2._Test__a

test1

同样地,在 _a 的情况下,该变量只是为了通知开发人员应将其用作该类的内部变量,即使您访问它,Python 解释器也不会执行任何操作,但这并不是一个好习惯。

testObj3 = Test()
print testObj3._a

test2

变量可以从任何地方访问,就像公共类变量一样。

testObj4 = Test()
print testObj4.a

test3

希望这个回答对你有所帮助 :)

5
所选答案很好地解释了属性如何消除对私有属性的需求,但我想补充一点,模块级别的函数也消除了对私有方法的需求。
如果将方法转换为模块级别的函数,则可以消除子类重写该方法的机会。将某些功能移动到模块级别比尝试隐藏方法更符合Python风格。

5

首先:为什么你想要隐藏你的数据?这很重要吗?

大部分情况下,你并不是真正想这样做,但因为其他人这样做你也会这样做。

如果你真的非常非常不想让别人使用某个东西,在它前面加上一个下划线即可。就是这样…… Python 程序员知道,带有一个下划线的东西不能保证每次都能工作,并且可能在不知情的情况下发生更改。

我们只能接受这种方式来生活,我们可以接受这一点。

使用两个下划线会使得你的类难以被子类化,甚至会让你自己都不想那样做。


2
你忽略了双下划线对于子类化的不良影响,如果加上这一点会更好地完善你的回答。 - Matt Joiner
2
鉴于双下划线实际上只是为了防止子类命名冲突(作为一种对子类说“不要碰”的方式),我不认为名称重整会造成问题。 - Russia Must Remove Putin

2
“如果对于一个变量是应该私有还是受保护存在疑问,最好选择私有。” - 是的,在Python中也是如此。
这里的一些答案提到了“惯例”,但没有给出这些惯例的链接。Python的权威指南PEP 8明确说明:
如果不确定,请选择非公共部分;将其公开比将公共属性变为非公共属性更容易。
Python中公共和私有的区别以及名称混淆已经在其他答案中讨论过。来自相同链接的引用:
我们在这里不使用“私有”一词,因为在Python中没有真正的私有属性(除非进行大量不必要的工作)。

2
乍一看应该与其他语言相同(在“其他”中我指的是Java或C++),但实际上不是这样。
在Java中,你可以将不应在外部访问的变量都设为private。而在Python中,由于没有“私有性”(正如Python原则之一所说:“我们都是成年人”),你无法做到这一点。因此双下划线只表示“伙计们,不要直接使用这个字段”。单下划线也具有相同的含义,在您需要从已考虑的类继承时不会引起任何问题。(这只是一个可能由双下划线引起的问题的示例)
因此,我建议您默认使用单下划线来表示“私有”成员。

使用双下划线表示“私有”,使用单下划线表示“受保护”。通常,人们只使用单下划线来表示所有内容(双下划线将有助于强制实施私有性,这通常违反Python风格)。 - Jonathan Sternberg
1
但是这不会使得双下划线类似于private,单下划线类似于protected吗?为什么不直接从“private”开始呢? - Paul Manta
@Paul 不,Python中没有private关键字,也不应该试图实现它。 - Roman Bodnarchuk
从概念上讲... 注意 'private' 两侧的引号。 - Paul Manta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接